{"id":33332,"date":"2026-05-01T21:34:53","date_gmt":"2026-05-01T19:34:53","guid":{"rendered":"https:\/\/www.angulararchitects.io\/?p=33332"},"modified":"2026-07-30T08:01:42","modified_gmt":"2026-07-30T06:01:42","slug":"custom-catalogs-in-a2ui-your-own-components-for-ai-generated-uis","status":"publish","type":"post","link":"https:\/\/www.angulararchitects.io\/en\/blog\/custom-catalogs-in-a2ui-your-own-components-for-ai-generated-uis\/","title":{"rendered":"Custom Catalogs in A2UI: Your Own Components for AI-Generated UIs"},"content":{"rendered":"<div class=\"wp-post-series-box series-agentic-angular wp-post-series-box--expandable\">\n\t\t\t<input id=\"collapsible-series-agentic-angular6a774727cb79a\" class=\"wp-post-series-box__toggle_checkbox\" type=\"checkbox\">\n\t\n\t<label\n\t\tclass=\"wp-post-series-box__label\"\n\t\t\t\t\tfor=\"collapsible-series-agentic-angular6a774727cb79a\"\n\t\t\ttabindex=\"0\"\n\t\t\t\t>\n\t\t<p class=\"wp-post-series-box__name wp-post-series-name\">\n\t\t\tThis is post 7 of 10 in the series <em>&ldquo;Agentic UI with Angular&rdquo;<\/em>\t\t<\/p>\n\t\t\t<\/label>\n\n\t\t\t<div class=\"wp-post-series-box__posts\">\n\t\t\t<ol>\n\t\t\t\t\t\t\t\t\t<li><a href=\"https:\/\/www.angulararchitects.io\/en\/blog\/understanding-ag-ui-the-standard-for-agentic-user-interfaces\/\">Understanding AG-UI: The Standard for Agentic User Interfaces<\/a><\/li>\n\t\t\t\t\t\t\t\t\t<li><a href=\"https:\/\/www.angulararchitects.io\/en\/blog\/ag-ui-in-practice-the-sdk-for-typescript\/\">AG-UI in Practice: The SDK for TypeScript<\/a><\/li>\n\t\t\t\t\t\t\t\t\t<li><a href=\"https:\/\/www.angulararchitects.io\/en\/blog\/ag-ui-in-practice-ag-ui-end-to-end-connecting-server-and-client\/\">AG-UI End to End: Connecting Server and Client<\/a><\/li>\n\t\t\t\t\t\t\t\t\t<li><a href=\"https:\/\/www.angulararchitects.io\/en\/blog\/implementing-ag-ui-with-angular\/\">Agentic UI with Angular, CopilotKit, and AG-UI<\/a><\/li>\n\t\t\t\t\t\t\t\t\t<li><a href=\"https:\/\/www.angulararchitects.io\/en\/blog\/a2ui-how-ai-generates-dynamic-uis-at-runtime\/\">A2UI: How AI Generates Dynamic UIs at Runtime<\/a><\/li>\n\t\t\t\t\t\t\t\t\t<li><a href=\"https:\/\/www.angulararchitects.io\/en\/blog\/integrating-a2ui-with-ag-ui-in-angular\/\">Integrating A2UI with AG-UI and CopilotKit in Angular<\/a><\/li>\n\t\t\t\t\t\t\t\t\t<li><span class=\"wp-post-series-box__current\">Custom Catalogs in A2UI: Your Own Components for AI-Generated UIs<\/span><\/li>\n\t\t\t\t\t\t\t\t\t<li><a href=\"https:\/\/www.angulararchitects.io\/en\/blog\/how-i-made-my-a2ui-dashboard-300-times-faster\/\">A2UI with a DSL: Controllable Dashboards Optimized for Performance<\/a><\/li>\n\t\t\t\t\t\t\t\t\t<li><a href=\"https:\/\/www.angulararchitects.io\/en\/blog\/agentic-ui-with-mcp-apps-tool-results-as-interactive-widgets\/\">Agentic UI with MCP Apps: Tool Results as Interactive Widgets<\/a><\/li>\n\t\t\t\t\t\t\t\t\t<li><a href=\"https:\/\/www.angulararchitects.io\/en\/blog\/mcp-apps-in-angular-with-copilotkit-rich-chat-interfaces-instead-of-text-responses\/\">MCP Apps in Angular with CopilotKit: Rich Chat Interfaces Instead of Text Responses<\/a><\/li>\n\t\t\t\t\t\t\t<\/ol>\n\t\t<\/div>\n\t<\/div>\n<p><em>Domain-specific widgets as first-class building blocks for the language model.<\/em><\/p>\n<p>As soon as domain-specific concepts come into play, A2UI's Basic Catalog rarely suffices: a flight booking, a boarding pass, or a bonus program quickly look arbitrary as generic cards and lists. With <em>Custom Catalogs<\/em>, A2UI therefore provides a clear mechanism to make your own domain-appropriate components and functions available to the language model \u2013 without sacrificing the lean, declarative character of the protocol.<\/p>\n<p>This third and final part of the series shows how to define such Custom Catalogs in Angular, register them with the renderer, and integrate them with the CopilotKit integration introduced in the second part.<\/p>\n<p>\ud83d\udcc2 <a href=\"https:\/\/github.com\/angular-architects\/flights42\/tree\/copilotkit\">Source Code<\/a> (see branch <code>copilotkit<\/code>; the standalone renderer example can be found in the folder <code>projects\/a2ui-demo<\/code>)<\/p>\n<h2>What Are Custom Catalogs in A2UI?<\/h2>\n<p>A <em>Custom Catalog<\/em> extends A2UI with your own, domain-driven components and functions that the language model may reference like any other component. Custom Catalogs often represent a superset of the Basic Catalog, so that in addition to your own building blocks they also include the well-known general-purpose UI building blocks. From the renderer's perspective, processing remains the same; the LLM merely receives a larger toolbox.<\/p>\n<p>In what follows, we extend the passenger card introduced in the first part with our own <code>MilesProgress<\/code> component, which visualizes the progress to the next bonus tier:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2026\/05\/miles-progress.png\" alt=\"Custom Catalog in A2UI: MilesProgress widget with a progress bar to the next bonus tier in an Angular demo\" style=\"max-width: 400px\" \/><\/p>\n<h2>Creating Your Own Components for the Custom Catalog<\/h2>\n<p>At its core, an A2UI component in Angular is just a regular Angular component. However, it receives the inputs that the agent transmits via A2UI through a defined <em>Context<\/em> object. The following listing shows the definition of such a context for our <code>MilesProgress<\/code> component. The <code>passenger<\/code> property is declared as a <code>BoundProperty<\/code>: it can hold either concrete data or represent a binding to the data model:<\/p>\n<pre><code class=\"language-typescript\">import type { BoundProperty } from &#039;@a2ui\/angular\/v0_9&#039;;\n\nexport interface MilesProgressContext {\n  passenger: BoundProperty&lt;Passenger&gt;;\n}<\/code><\/pre>\n<p>The corresponding component receives this context via the <code>props<\/code> <code>InputSignal<\/code>:<\/p>\n<pre><code class=\"language-typescript\">@Component({\n  selector: &#039;app-miles-progress&#039;,\n  changeDetection: ChangeDetectionStrategy.OnPush,\n  imports: [DecimalPipe],\n  template: `\n    &lt;section class=&quot;miles-progress&quot;&gt;\n      &lt;p class=&quot;eyebrow&quot;&gt;Miles Progress&lt;\/p&gt;\n      &lt;p class=&quot;current&quot;&gt;{{ passenger().bonusMiles | number }}&lt;\/p&gt;\n      &lt;p class=&quot;remaining&quot;&gt;\n        {{ remainingMiles() | number }} miles to {{ nextThreshold() | number }}\n      &lt;\/p&gt;\n      &lt;div aria-hidden=&quot;true&quot; class=&quot;track&quot;&gt;\n        &lt;div class=&quot;fill&quot; [style.width.%]=&quot;progressPercent()&quot;&gt;&lt;\/div&gt;\n      &lt;\/div&gt;\n    &lt;\/section&gt;\n  `,\n  styleUrl: &#039;.\/miles-progress.css&#039;,\n})\nexport class MilesProgress {\n  readonly props = input&lt;MilesProgressContext&gt;(initialContext);\n  readonly surfaceId = input.required&lt;string&gt;();\n  readonly componentId = input.required&lt;string&gt;();\n  readonly dataContextPath = input(&#039;\/&#039;);\n\n  protected readonly passenger = computed(() =&gt; this.props().passenger.value());\n\n  protected readonly nextThreshold = computed(() =&gt;\n    calcNextThreshold(this.passenger().bonusMiles),\n  );\n\n  protected readonly remainingMiles = computed(() =&gt;\n    calcRemainingMiles(this.nextThreshold(), this.passenger().bonusMiles),\n  );\n\n  protected readonly progressPercent = computed(() =&gt;\n    calcProgressPercent(this.nextThreshold(), this.passenger().bonusMiles),\n  );\n}<\/code><\/pre>\n<p>Besides <code>props<\/code>, the renderer sets three further inputs when it creates the component dynamically: <code>surfaceId<\/code> names the surface the component belongs to, <code>componentId<\/code> identifies it within that surface, and <code>dataContextPath<\/code> provides the base path in the data model against which relative bindings are resolved. Our <code>MilesProgress<\/code> component does not need this information; it becomes interesting for widgets that write to the data model themselves or trigger actions.<\/p>\n<p>Currently, there is no dedicated interface for this set of inputs.<\/p>\n<p>The <code>MilesProgress<\/code> component shown reads the current bonus miles from the context and uses a computed signal to calculate the number of miles still needed to reach the next bonus tier. It also displays a progress indicator for that goal.<\/p>\n<p>So that the renderer knows the <code>MilesProgress<\/code> component and can instantiate it correctly, a description of its inputs in the form of a schema is also required. The renderer from the A2UI team uses the popular library <a href=\"https:\/\/zod.dev\/\">Zod<\/a> for this:<\/p>\n<pre><code class=\"language-typescript\">import type { AngularComponentImplementation } from &#039;@a2ui\/angular\/v0_9&#039;;\nimport { z } from &#039;zod\/v3&#039;;\n\n[...]\n\nconst passengerSchema = z.object({\n  id: z.number(),\n  firstName: z.string(),\n  lastName: z.string(),\n  bonusMiles: z.number(),\n});\n\nconst milesProgressSchema = z\n  .object({\n    passenger: binding(passengerSchema).optional(),\n  })\n  .strict();\n\nexport const milesProgressEntry = {\n  name: &#039;MilesProgress&#039;,\n  component: MilesProgress,\n  schema: milesProgressSchema as unknown,\n} as unknown as AngularComponentImplementation;\n\/\/ ^^^ Cast works around a typing issue in the current version<\/code><\/pre>\n<p>The schema specifies that the <code>passenger<\/code> property can be passed either as an object (<code>passengerSchema<\/code>) or as a data binding with a <code>path<\/code> property. The constant <code>milesProgressEntry<\/code> bundles the component's name and implementation as well as the schema into a single unit that the catalog can later include.<\/p>\n<p>Since every property may take either a concrete value or a data binding, the example project ships a small helper function <code>binding<\/code> that pairs a value schema with the alternative of a <code>path<\/code> binding:<\/p>\n<pre><code class=\"language-typescript\">export function binding&lt;T extends z.ZodTypeAny&gt;(schema: T) {\n  return z.union([schema, z.object({ path: z.string() }).strict()]);\n}<\/code><\/pre>\n<p><div style=\"\nmargin: 8px 0;\npadding: 22px;\nborder: 1px solid #e5e7eb;\nborder-radius: 14px;\nbackground: #f8fafc;\n\">NOTE<\/p>\n<h3 style=\"margin-top:0\">New: Agentic UI with Angular<\/h3>\n<p>If you don\u2019t just want to integrate A2UI but embed it cleanly into larger architectures:<br \/>\nIn my book Agentic UI with Angular, I cover exactly these patterns and trade-offs in depth.<\/p>\n<p><a href=\"https:\/\/agentic-angular.com\/\"><img decoding=\"async\" src=\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2026\/04\/cover.png\" width=\"400\" alt=\"Cover of the eBook Agentic UI with Angular\" style=\"cursor:pointer !important\"><\/a><\/p>\n<p><a style=\"cursor:pointer !important\" href=\"https:\/\/agentic-angular.com\/\">Learn more about the eBook \u2192<\/a>\n<\/div>\n<\/p>\n<h2>Your Own Functions for the Custom Catalog<\/h2>\n<p>In addition to components, a Custom Catalog can also provide its own functions. These complement the standard functions included in the Basic Catalog, such as <code>formatNumber<\/code> and <code>formatDate<\/code>. The following listing shows a small helper function <code>formatId<\/code>, which converts a numeric ID into a readable string like <code>P-0042<\/code>. The factory <code>createFunctionImplementation<\/code> from <code>@a2ui\/web_core<\/code> takes the metadata \u2013 name, return type, and a Zod schema for the expected arguments \u2013 as well as the actual implementation:<\/p>\n<pre><code class=\"language-typescript\">import {\n  createFunctionImplementation,\n  type FunctionImplementation,\n} from &#039;@a2ui\/web_core\/v0_9&#039;;\nimport { z } from &#039;zod\/v3&#039;;\n\nconst formatIdSchema = z\n  .object({\n    value: z.number(),\n  })\n  .strict();\n\nexport const formatIdImplementation = createFunctionImplementation(\n  {\n    name: &#039;formatId&#039;,\n    returnType: &#039;string&#039;,\n    schema: formatIdSchema as unknown as FunctionImplementation[&#039;schema&#039;],\n  },\n  ({ value }) =&gt; {\n    const normalizedValue = Math.max(0, Math.trunc(value));\n\n    return `P-${String(normalizedValue).padStart(4, &#039;0&#039;)}`;\n  },\n);<\/code><\/pre>\n<p>With components and functions, the two central building blocks of a Custom Catalog are defined. As the next step, we need to make the catalog available to the renderer.<\/p>\n<h2>Registering a Custom Catalog with the A2UI Renderer<\/h2>\n<p>A Custom Catalog is an instance of <code>BasicCatalogBase<\/code> that receives a unique id, the list of additional components, and a list of functions in its constructor:<\/p>\n<pre><code class=\"language-typescript\">import { BASIC_FUNCTIONS, BasicCatalogBase } from &#039;@a2ui\/angular\/v0_9&#039;;\n\nimport { formatIdImplementation } from &#039;.\/format-id&#039;;\nimport { milesProgressEntry } from &#039;.\/miles-progress&#039;;\n\nexport const customCatalog = new BasicCatalogBase({\n  id: &#039;https:\/\/example.com\/catalogs\/flights42-a2ui-demo&#039;,\n  extraComponents: [milesProgressEntry],\n  functions: [...BASIC_FUNCTIONS, formatIdImplementation],\n});<\/code><\/pre>\n<p>Unfortunately, the API behaves somewhat asymmetrically in the version discussed here: <code>extraComponents<\/code> complements the standard components, whereas <code>functions<\/code> replaces the standard functions, which is why <code>BASIC_FUNCTIONS<\/code> has to be spread in by hand.<\/p>\n<p>So that the renderer uses the new catalog, the instance just needs to be referenced in the configuration \u2013 a dedicated Angular service is not required for this:<\/p>\n<pre><code class=\"language-typescript\">import {\n  A2UI_RENDERER_CONFIG,\n  A2uiRendererService,\n  provideMarkdownRenderer,\n} from &#039;@a2ui\/angular\/v0_9&#039;;\nimport {\n  ApplicationConfig,\n  provideBrowserGlobalErrorListeners,\n} from &#039;@angular\/core&#039;;\nimport { marked } from &#039;marked&#039;;\n\nimport { customCatalog } from &#039;.\/custom-catalog\/custom-catalog&#039;;\n\nexport const appConfig: ApplicationConfig = {\n  providers: [\n    provideBrowserGlobalErrorListeners(),\n    {\n      provide: A2UI_RENDERER_CONFIG,\n      useValue: {\n        catalogs: [customCatalog],\n      },\n    },\n    provideMarkdownRenderer(async (markdown) =&gt;\n      marked.parse(String(markdown ?? &#039;&#039;)),\n    ),\n    A2uiRendererService,\n  ],\n};<\/code><\/pre>\n<p>Once the catalog is registered, the example application can use the <code>MilesProgress<\/code> component like any other component in an A2UI message. The following listing shows an excerpt of an <code>updateComponents<\/code> message in which the <code>MilesProgress<\/code> component is displayed alongside the existing passenger card:<\/p>\n<pre><code class=\"language-typescript\">updateComponents: {\n  surfaceId,\n  components: [\n    {\n      id: &#039;root&#039;,\n      component: &#039;Column&#039;,\n      children: [&#039;passenger-card&#039;, &#039;miles-progress&#039;],\n    },\n    [...]\n    {\n      id: &#039;miles-progress&#039;,\n      component: &#039;MilesProgress&#039;,\n      passenger: { path: &#039;\/passenger&#039; },\n    },\n  ],\n}<\/code><\/pre>\n<h2>Wiring Custom Components into the CopilotKit Integration<\/h2>\n<p>So far we've registered Custom Components for the standalone A2UI renderer. In combination with the CopilotKit integration introduced in the second part, the approach looks similar but somewhat more convenient: in the folder <code>util-copilotkit<\/code>, the example project provides its own helper functions that encapsulate both the component description and its registration. How these helpers are built internally is shown in the section &quot;Under the Hood: The Schema Helpers in Detail&quot; at the end of this article.<\/p>\n<p>So that the A2UI renderer in the sidecar can show not only the components of the Basic Catalog but also your own widgets, Custom Components can be added. The example project used here contains a <code>TicketWidget<\/code> that represents a boarding pass:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2026\/05\/custom-a2ui-component.png\" alt=\"Custom Catalog in A2UI: TicketWidget as a boarding pass with flight number, route, and date, embedded in a sidecar chat\" style=\"max-width: 400px\" \/><\/p>\n<p>The helper function used here, <code>createCustomComponent<\/code>, takes the name, the description, the component implementation, and a Zod schema that describes the component's properties:<\/p>\n<pre><code class=\"language-typescript\">import { z } from &#039;zod\/v3&#039;;\n\nimport {\n  binding,\n  createCustomComponent,\n} from &#039;..\/..\/..\/shared\/util-copilotkit\/a2ui\/a2ui-schema&#039;;\nimport { A2uiCustomCatalogComponent } from &#039;..\/..\/..\/shared\/util-copilotkit\/a2ui\/types&#039;;\nimport { TicketWidget } from &#039;.\/ticket\/ticket-widget&#039;;\n\nexport const ticketWidgetEntry = createCustomComponent({\n  name: &#039;TicketWidget&#039;,\n  description: &#039;A boarding-pass-style widget ...&#039;,\n  component: TicketWidget,\n  schema: z\n    .object({\n      ticketId: binding(z.union([z.string(), z.number()])),\n      from: binding(z.string()),\n      to: binding(z.string()),\n      date: binding(z.string()),\n      delay: binding(z.number()).optional(),\n    })\n    .strict(),\n});\n\nexport const ticketingExtraComponents: A2uiCustomCatalogComponent[] = [\n  ticketWidgetEntry,\n];<\/code><\/pre>\n<p>The helper function <code>binding<\/code> corresponds to the variant shown earlier: it marks those fields that the LLM may either set directly or wire to values from the data model via a <code>path<\/code> binding. In addition, <code>createCustomComponent<\/code> uses its type parameters to ensure that the <code>props<\/code> signal of the given component matches the schema.<\/p>\n<p>By the way, it's worth taking a look at the full <code>description<\/code> in the example project: it not only describes what the widget displays, but also gives the language model clear usage rules \u2013 for instance, that it should only use the <code>TicketWidget<\/code> when explicitly asked, and at most once per request. The description thus becomes a building block of the prompt.<\/p>\n<p>The helper function <code>createCustomCatalog<\/code> bundles the components and the catalog id into a catalog descriptor:<\/p>\n<pre><code class=\"language-typescript\">import { createCustomCatalog } from &#039;..\/..\/..\/shared\/util-copilotkit\/a2ui\/types&#039;;\nimport { ticketingExtraComponents } from &#039;.\/ticketing-extra-components&#039;;\n\nexport const customCatalog = createCustomCatalog({\n  id: &#039;https:\/\/example.com\/catalogs\/flights42-a2ui-demo&#039;,\n  components: ticketingExtraComponents,\n});<\/code><\/pre>\n<p>As the id, the example project assigns \u2013 as is customary for A2UI catalogs \u2013 a URL from its own domain in order to guarantee uniqueness. This id is more than just a label: the renderer looks up the catalog by its id, and the agent has to use exactly this id in its <code>createSurface<\/code> operations. To make this work, the client transmits the id together with the catalog description to the agent; how it makes its way into the system prompt there is shown in the next sections.<\/p>\n<p>For registration, the function <code>provideA2uiCatalog<\/code> known from the second part is sufficient, which now takes the catalog descriptor:<\/p>\n<pre><code class=\"language-typescript\">import { provideMarkdownRenderer } from &#039;@a2ui\/angular\/v0_9&#039;;\nimport { provideCopilotKit } from &#039;@copilotkit\/angular&#039;;\nimport { marked } from &#039;marked&#039;;\n\nimport { a2uiActivityRendererConfig } from &#039;.\/domains\/shared\/util-copilotkit\/a2ui\/a2ui-activity-renderer&#039;;\nimport { provideA2uiCatalog } from &#039;.\/domains\/shared\/util-copilotkit\/a2ui\/provide-a2ui-catalog&#039;;\nimport { customCatalog } from &#039;.\/domains\/ticketing\/ai\/custom-catalog\/catalog&#039;;\n\n[...]\n\nexport const appConfig: ApplicationConfig = {\n  providers: [\n    [...]\n    provideCopilotKit({\n      renderActivityMessages: [a2uiActivityRendererConfig],\n    }),\n    provideA2uiCatalog(customCatalog),\n    provideMarkdownRenderer(async (markdown) =&gt;\n      marked.parse(String(markdown ?? &#039;&#039;)),\n    ),\n  ],\n};<\/code><\/pre>\n<p>Without arguments, <code>provideA2uiCatalog<\/code> merely registers the Basic Catalog. Given a descriptor, the function extends this catalog with the provided components and functions:<\/p>\n<pre><code class=\"language-typescript\">export const A2UI_CUSTOM_CATALOG = new InjectionToken&lt;A2uiCustomCatalog&gt;(\n  &#039;A2UI_CUSTOM_CATALOG&#039;,\n);\n\nexport function provideA2uiCatalog(\n  catalog?: A2uiCustomCatalog,\n  options?: ProvideA2uiCatalogOptions,\n): EnvironmentProviders {\n  if (!catalog) {\n    return makeEnvironmentProviders([\n      {\n        provide: A2UI_RENDERER_CONFIG,\n        useFactory: (): RendererConfiguration =&gt; ({\n          catalogs: [inject(BasicCatalog)],\n        }),\n      },\n      A2uiRendererService,\n    ]);\n  }\n\n  const { sendCatalogDescription = true } = options ?? {};\n\n  const rendererCatalog = new BasicCatalogBase({\n    id: catalog.id,\n    extraComponents: catalog.components.map(toAngularComponentImplementation),\n    functions: [\n      ...BASIC_FUNCTIONS,\n      ...(catalog.functions ?? []).map(toFunctionImplementation),\n    ],\n  });\n\n  const storedCatalog: A2uiCustomCatalog = sendCatalogDescription\n    ? catalog\n    : { id: catalog.id, components: [] };\n\n  return makeEnvironmentProviders([\n    { provide: A2UI_CUSTOM_CATALOG, useValue: storedCatalog },\n    {\n      provide: A2UI_RENDERER_CONFIG,\n      useValue: { catalogs: [rendererCatalog] },\n    },\n    A2uiRendererService,\n  ]);\n}<\/code><\/pre>\n<p>The local helpers <code>toAngularComponentImplementation<\/code> and <code>toFunctionImplementation<\/code> are plain remappings of the descriptor entries onto the renderer structures shown at the beginning; their details can be found in the source code. The function also takes care of merging in <code>BASIC_FUNCTIONS<\/code> \u2013 the asymmetry mentioned earlier thus remains an implementation detail. In addition, it stores the descriptor in the injection token <code>A2UI_CUSTOM_CATALOG<\/code>; the next section shows the role this plays. The option <code>sendCatalogDescription<\/code> determines whether a description of the entire catalog or only its id is sent to the server.<\/p>\n<p>The Activity Renderer introduced in the second part remains untouched by all of this: it still forwards the received A2UI operations to the <code>A2uiRendererService<\/code>, which resolves the Custom Components via the registered catalog.<\/p>\n<h2>Telling the Agent About Custom Components<\/h2>\n<p>The renderer can now display the <code>TicketWidget<\/code> \u2013 but how does the language model know that this component exists in the first place? This task is handled by the glue function <code>initAgentStore<\/code> introduced in the second part: when registering an agent, it reads the descriptor stored at the injection token <code>A2UI_CUSTOM_CATALOG<\/code> and registers it as a context entry for that agent (abridged):<\/p>\n<pre><code class=\"language-typescript\">import { type Context } from &#039;@ag-ui\/core&#039;;\nimport { inject } from &#039;@angular\/core&#039;;\nimport { connectAgentContext } from &#039;@copilotkit\/angular&#039;;\n\nimport {\n  catalogIdToContextEntry,\n  catalogToContextEntry,\n} from &#039;.\/a2ui\/catalog-context&#039;;\nimport { A2UI_CUSTOM_CATALOG } from &#039;.\/a2ui\/provide-a2ui-catalog&#039;;\n\n[...]\n\nexport function initAgentStore(config: InitAgentStoreConfig): void {\n  [...]\n\n  connectCatalogContext(config.agentId, config.catalogIdOnly ?? false);\n\n  [...]\n}\n\nfunction connectCatalogContext(agentId: string, idOnly: boolean): void {\n  const catalog = inject(A2UI_CUSTOM_CATALOG, { optional: true });\n  if (!catalog) {\n    return;\n  }\n\n  const entry = idOnly\n    ? catalogIdToContextEntry(catalog.id)\n    : catalogToContextEntry(catalog);\n\n  connectAgentContext(() =&gt; ({ ...entry, agentIds: [agentId] }) as Context);\n}<\/code><\/pre>\n<p>Context entries are a generic mechanism provided by AG-UI for passing additional information to the agent. The function <code>connectAgentContext<\/code> from <code>@copilotkit\/angular<\/code> registers a factory with the CopilotKit runtime, whose result flows into the context when the requests are assembled. The property <code>agentIds<\/code> restricts the entry to the agent just registered \u2013 in an application with several agents, each one thus receives exactly its own catalog entry. Since the catalog remains unchanged at runtime, it is serialized once during initialization.<\/p>\n<p>The individual agent stores therefore get by without any catalog knowledge: the function <code>injectTicketingAgentStore<\/code> shown in the second part remains unchanged; the catalog flows in automatically as soon as <code>provideA2uiCatalog<\/code> has stored it at the injection token. For agents that don't need any component descriptions but merely have to reference the catalog id, <code>initAgentStore<\/code> additionally offers the option <code>catalogIdOnly: true<\/code>.<\/p>\n<p>The serialization is handled by the helper function <code>catalogToContextEntry<\/code>: it converts the catalog id, the names and descriptions of the Custom Components, and \u2013 with the help of <code>zodToJsonSchema<\/code> \u2013 their schemas into a context entry. Its implementation can also be found in the section at the end of this article.<\/p>\n<h2>The Server Perspective: Using the Custom Catalog from Context<\/h2>\n<p>On the server side, the agent has to evaluate the received context entry again. In the example project, this task is handled by the function <code>addCustomCatalogInstructions<\/code> from the folder <code>libs\/ag-ui-server<\/code>. The Mastra agent used here wires it directly into its <code>instructions<\/code>:<\/p>\n<pre><code class=\"language-typescript\">export const ticketingAgent = new Agent({\n  id: &#039;ticketingAgent&#039;,\n  name: &#039;Flight42 Ticketing Assistant&#039;,\n  instructions: addCustomCatalogInstructions({\n    systemInstructions: ticketingAgentPrompt,\n  }),\n  [...]\n});<\/code><\/pre>\n<p>Noteworthy here is the signature of <code>ticketingAgentPrompt<\/code>: the system prompt is no longer a static string but a factory that takes the catalog id and weaves it into the instructions \u2013 for instance where the prompt prescribes the structure of the <code>createSurface<\/code> operations:<\/p>\n<pre><code class=\"language-typescript\">export function ticketingAgentPrompt(catalogId: string): string {\n  return `\n[...]\n- renderA2uiTool expects { messages: A2uiMessage[] } \u2014 one self-contained A2UI\n  v0.9 surface that MUST contain:\n  - one createSurface message with a fresh surfaceId and catalogId\n    &quot;${catalogId}&quot;;\n[...]\n`;\n}<\/code><\/pre>\n<p>Behind the call to <code>addCustomCatalogInstructions<\/code> sits an <code>instructions<\/code> factory that extracts the catalog id from the runtime context, builds the base prompt with it, and appends the component description as an additional section (abridged):<\/p>\n<pre><code class=\"language-typescript\">import {\n  A2UI_DEFAULT_CATALOG_ID,\n  catalogToPromptSection,\n  extractCatalogId,\n} from &#039;.\/catalog-to-prompt.js&#039;;\n\n[...]\n\nexport interface AddCustomCatalogInstructionsOptions {\n  \/** Builds the system prompt for the catalog id the client registered. *\/\n  systemInstructions: (catalogId: string) =&gt; string;\n  [...]\n}\n\nexport function addCustomCatalogInstructions(\n  options: AddCustomCatalogInstructionsOptions,\n): (params: InstructionsParams) =&gt; string {\n  const { systemInstructions } = options;\n\n  return ({ requestContext }) =&gt; {\n    const agUi = requestContext.get(&#039;ag-ui&#039;) as AgUiRuntimeContext | undefined;\n    const catalogId =\n      extractCatalogId(agUi?.context) ?? A2UI_DEFAULT_CATALOG_ID;\n    const catalogSection = catalogToPromptSection(agUi?.context);\n    const baseInstructions = systemInstructions(catalogId);\n\n    return catalogSection\n      ? `${baseInstructions}\\n\\n${catalogSection}`\n      : baseInstructions;\n  };\n}<\/code><\/pre>\n<p>The context entries transmitted by AG-UI are available under the key <code>ag-ui<\/code> in Mastra's runtime context. The helper function <code>extractCatalogId<\/code> reads the id of the client-side registered catalog from it; if the client does not announce a Custom Catalog, <code>A2UI_DEFAULT_CATALOG_ID<\/code> kicks in as a fallback with the id of the Basic Catalog. The helper function <code>catalogToPromptSection<\/code> looks through the same context entries for the entry with the description <code>A2UI Custom Catalog<\/code>, parses the serialized catalog definition, and turns it into a prompt section. This section lists the available Custom Components including their descriptions and derives simple example props from the JSON schemas that the model can orient itself on.<\/p>\n<p>This closes the loop: the client describes its Custom Components together with the catalog id, the agent takes both into the prompt, the language model references id and components in its A2UI messages, and the renderer displays them via the registered catalog.<\/p>\n<h2>Security Aspect: <code>sendCatalogDescription<\/code> and Prompt Injection<\/h2>\n<p>By default, the solution shown transmits the textual descriptions and schemas of the components to the agent, which embeds them into the system prompt. This approach is very convenient for development, but in production it can be abused for prompt injection. In such an attack, an attacker injects malicious instructions into the system prompt and thereby tricks the language model into performing unintended actions.<\/p>\n<p>Therefore, in production it is advisable to set the <code>sendCatalogDescription<\/code> option of <code>provideA2uiCatalog<\/code> to <code>false<\/code>:<\/p>\n<pre><code class=\"language-typescript\">provideA2uiCatalog(customCatalog, { sendCatalogDescription: false }),<\/code><\/pre>\n<p>In this case, the client keeps the full catalog definition for local rendering only; as a context entry it transmits merely the catalog id, which the agent needs for its <code>createSurface<\/code> operations anyway. The agent instead obtains the catalog's schema based on this id from a trusted registry \u2013 for example, via an internal API or database \u2013 validating the id against a list of approved catalogs in the process.<\/p>\n<h2>Under the Hood: The Schema Helpers in Detail<\/h2>\n<p>To conclude, it's worth taking a look at the implementation of those helper functions that take care of the Custom Catalog's schema on the client side. Anyone who simply adopts them from the example project can safely skip this section; anyone who wants to adapt them to their own project will find the central building blocks here.<\/p>\n<p>The implementation of <code>createCustomComponent<\/code> is deliberately plain:<\/p>\n<pre><code class=\"language-typescript\">export interface CustomCatalogEntry&lt;\n  TName extends string = string,\n  TSchema extends z.ZodObject&lt;z.ZodRawShape&gt; = z.ZodObject&lt;z.ZodRawShape&gt;,\n> {\n  name: TName;\n  description: string;\n  schema: TSchema;\n  component: Type&lt;{\n    props: Signal&lt;ContextFromSchema&lt;TSchema&gt;&gt;;\n  }&gt;;\n}\n\nexport function createCustomComponent&lt;\n  const TName extends string,\n  const TSchema extends z.ZodObject&lt;z.ZodRawShape&gt;,\n>(\n  entry: CustomCatalogEntry&lt;TName, TSchema&gt;,\n): CustomCatalogEntry&lt;TName, TSchema&gt; {\n  return entry;\n}<\/code><\/pre>\n<p>The function returns the given entry unchanged \u2013 its value lies in the type checking: the mapped type <code>ContextFromSchema<\/code> derives from the Zod schema the context type that the component has to accept via its <code>props<\/code> signal; each property is expected as a <code>BoundProperty<\/code>. If the schema does not match the component, compilation already fails.<\/p>\n<p><code>createCustomCatalog<\/code> is likewise a pure typing helper that returns the given descriptor unchanged. Besides the id, the descriptor comprises the components as well as optional functions:<\/p>\n<pre><code class=\"language-typescript\">export interface A2uiCustomCatalog {\n  id: string;\n  components: A2uiCustomCatalogComponent[];\n  functions?: A2uiCustomCatalogFunction[];\n}<\/code><\/pre>\n<p>Finally, the function <code>catalogToContextEntry<\/code> serializes this descriptor for transmission to the agent:<\/p>\n<pre><code class=\"language-typescript\">import { type Context } from &#039;@ag-ui\/core&#039;;\nimport { zodToJsonSchema } from &#039;zod-to-json-schema&#039;;\n\nimport { type A2uiCustomCatalog } from &#039;.\/types&#039;;\n\nexport const A2UI_CATALOG_CONTEXT_DESCRIPTION = &#039;A2UI Custom Catalog&#039;;\n\nexport function catalogToContextEntry(catalog: A2uiCustomCatalog): Context {\n  const components = Object.fromEntries(\n    catalog.components.map((component) =&gt; [\n      component.name,\n      {\n        description: component.description,\n        schema: zodToJsonSchema(component.schema, { $refStrategy: &#039;none&#039; }),\n      },\n    ]),\n  );\n\n  return {\n    description: A2UI_CATALOG_CONTEXT_DESCRIPTION,\n    value: JSON.stringify({ catalogId: catalog.id, components }),\n  };\n}<\/code><\/pre>\n<p>The option <code>$refStrategy: &#039;none&#039;<\/code> keeps the generated JSON schemas free of <code>$ref<\/code> references, so that the server side can process them further without additional resolution. The constant <code>A2UI_CATALOG_CONTEXT_DESCRIPTION<\/code> serves as the identifying marker: based on exactly this description, the server side identifies the catalog entry among the transmitted context entries.<\/p>\n<p>It's worth noting that the function also yields an entry for a catalog without components: this way the catalog id reaches the server in any case \u2013 for instance when <code>sendCatalogDescription: false<\/code> withholds the descriptions. For agents that only need the id in the first place, the example project additionally offers the short form <code>catalogIdToContextEntry(catalogId)<\/code>, which <code>initAgentStore<\/code> uses when the option <code>catalogIdOnly: true<\/code> is set.<\/p>\n<h2>Summary<\/h2>\n<p>Custom Catalogs extend A2UI in a targeted way with your own components and functions that the language model may use like any other building block. This makes it possible to translate generic responses into domain-appropriate interfaces without losing the character of a lean, declarative protocol. Schema validation via Zod ensures clean contracts between agent and client, while the split into components and functions keeps the catalog flexible.<\/p>\n<p>In combination with the CopilotKit integration, Custom Components can be described compactly with <code>createCustomComponent<\/code> and wired in through a single call to <code>provideA2uiCatalog<\/code>. The catalog description travels, together with the catalog id, to the agent as an AG-UI context entry; the agent weaves the id into its system prompt and uses it in its <code>createSurface<\/code> operations from then on \u2013 the Activity Renderer from the second part remains unchanged in the process. Anyone planning the move to production should also consider the security aspect around <code>sendCatalogDescription<\/code> and source catalog schemas from a trusted source.<\/p>\n<p>Anyone who combines protocol, renderer, CopilotKit integration, and Custom Catalogs has a solid foundation to have language models deliver not just text but real UI responses \u2013 and in a way that fits their own application.<\/p>\n<hr \/>\n<p><div style=\"\nmargin: 8px 0;\npadding: 22px;\nborder: 1px solid #e5e7eb;\nborder-radius: 14px;\nbackground: #f8fafc;\n\"><\/p>\n<h3 style=\"margin-top:0\">Interested in production-ready Agentic UI architectures?<\/h3>\n<p>In my workshop, we dig into AG-UI, A2UI, MCP Apps, HITL patterns, and modern Angular architectures for real-world agentic systems.<\/p>\n<p><a href=\"https:\/\/www.angulararchitects.io\/en\/training\/agentic-ai-with-angular-architecture-patterns\/\"><img decoding=\"async\" src=\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2026\/05\/sujet-workshop-en.png\" width=\"400\" alt=\"Workshop: Agentic AI with Angular \u2013 AG-UI, A2UI, MCP Apps & HITL patterns\" style=\"cursor:pointer !important\"><\/a><\/p>\n<p><a style=\"cursor:pointer !important\" href=\"https:\/\/www.angulararchitects.io\/en\/training\/agentic-ai-with-angular-architecture-patterns\/\">See all details \u2192<\/a>\n<\/div>\n<\/p>\n<h2>FAQ<\/h2>\n<h3>What is a Custom Catalog in A2UI?<\/h3>\n<p>A Custom Catalog extends A2UI with your own, domain-driven components and functions. It often includes the Basic Catalog as a superset and additionally provides domain-specific building blocks to the language model, which the renderer processes like any other A2UI component.<\/p>\n<h3>How do you describe your own A2UI component?<\/h3>\n<p>The implementation is a regular Angular component that receives its inputs via a <code>Context<\/code> object. In addition to the component itself, a Zod schema is defined that describes the expected properties. Component, name, and schema are added together as an entry in the Custom Catalog \u2013 in the CopilotKit integration in a type-safe way via the helper function <code>createCustomComponent<\/code>.<\/p>\n<h3>How do you register a Custom Catalog in Angular?<\/h3>\n<p>For the standalone renderer, an instance of <code>BasicCatalogBase<\/code> referenced in the <code>A2UI_RENDERER_CONFIG<\/code> token is sufficient. In the CopilotKit integration, this is handled by the <code>provideA2uiCatalog<\/code> function, to which a descriptor created with <code>createCustomCatalog<\/code>, containing components and optional functions, can be passed directly.<\/p>\n<h3>How does the agent learn about the Custom Components?<\/h3>\n<p>The glue function <code>initAgentStore<\/code>, which registers the agent stores, reads the catalog stored at the token <code>A2UI_CUSTOM_CATALOG<\/code>, serializes it with <code>catalogToContextEntry<\/code>, and registers it via <code>connectAgentContext<\/code> as an AG-UI context entry for the respective agent. On the server side, <code>addCustomCatalogInstructions<\/code> extracts the catalog id, builds the system prompt with it, and appends the component descriptions along with example props derived from them.<\/p>\n<h3>What is <code>sendCatalogDescription<\/code> for and when should you disable the option?<\/h3>\n<p>By default (<code>sendCatalogDescription: true<\/code>), the client transmits the component descriptions and schemas to the agent, which embeds them into the system prompt. That is very convenient, but it can lead to prompt injection. In production, it is therefore advisable to set the option to <code>false<\/code>: the client then only transmits the catalog id, and the server obtains the schemas from a trusted registry.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>This is post 7 of 10 in the series &ldquo;Agentic UI with Angular&rdquo; Understanding AG-UI: The Standard for Agentic User Interfaces AG-UI in Practice: The SDK for TypeScript AG-UI End to End: Connecting Server and Client Agentic UI with Angular, CopilotKit, and AG-UI A2UI: How AI Generates Dynamic UIs at Runtime Integrating A2UI with AG-UI [&hellip;]<\/p>\n","protected":false},"author":25,"featured_media":33334,"comment_status":"open","ping_status":"open","sticky":true,"template":"","format":"standard","meta":{"_acf_changed":false,"_price":"","_stock":"","_tribe_ticket_header":"","_tribe_default_ticket_provider":"","_ticket_start_date":"","_ticket_end_date":"","_tribe_ticket_show_description":"","_tribe_ticket_show_not_going":false,"_tribe_ticket_use_global_stock":"","_tribe_ticket_global_stock_level":"","_global_stock_mode":"","_global_stock_cap":"","_tribe_rsvp_for_event":"","_tribe_ticket_going_count":"","_tribe_ticket_not_going_count":"","_tribe_tickets_list":"[]","_tribe_ticket_has_attendee_info_fields":false,"footnotes":""},"categories":[18],"tags":[],"class_list":["post-33332","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized","post_series-agentic-angular"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.1.1 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Custom Catalogs in A2UI: Your Own Components for AI-Generated UIs - ANGULARarchitects<\/title>\n<meta name=\"description\" content=\"How do you extend A2UI with your own domain-specific components? Using Angular as an example, this article shows how to define Custom Catalogs, secure them with Zod schemas, register them with the renderer, and wire them into the CopilotKit integration from the second part.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.angulararchitects.io\/en\/blog\/custom-catalogs-in-a2ui-your-own-components-for-ai-generated-uis\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Custom Catalogs in A2UI: Your Own Components for AI-Generated UIs - ANGULARarchitects\" \/>\n<meta property=\"og:description\" content=\"How do you extend A2UI with your own domain-specific components? Using Angular as an example, this article shows how to define Custom Catalogs, secure them with Zod schemas, register them with the renderer, and wire them into the CopilotKit integration from the second part.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.angulararchitects.io\/en\/blog\/custom-catalogs-in-a2ui-your-own-components-for-ai-generated-uis\/\" \/>\n<meta property=\"og:site_name\" content=\"ANGULARarchitects\" \/>\n<meta property=\"article:published_time\" content=\"2026-05-01T19:34:53+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-07-30T06:01:42+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2026\/05\/series-1024x536.png\" \/>\n<meta name=\"author\" content=\"Manfred Steyer\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:image\" content=\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2026\/05\/series-1024x536.png\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Manfred Steyer\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"20 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\/\/www.angulararchitects.io\/en\/blog\/custom-catalogs-in-a2ui-your-own-components-for-ai-generated-uis\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/www.angulararchitects.io\/en\/blog\/custom-catalogs-in-a2ui-your-own-components-for-ai-generated-uis\/\"},\"author\":{\"name\":\"Manfred Steyer\",\"@id\":\"https:\/\/www.angulararchitects.io\/en\/#\/schema\/person\/f3de69c1e2bdb5ba04d8d2f5f998b81a\"},\"headline\":\"Custom Catalogs in A2UI: Your Own Components for AI-Generated UIs\",\"datePublished\":\"2026-05-01T19:34:53+00:00\",\"dateModified\":\"2026-07-30T06:01:42+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/www.angulararchitects.io\/en\/blog\/custom-catalogs-in-a2ui-your-own-components-for-ai-generated-uis\/\"},\"wordCount\":2778,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\/\/www.angulararchitects.io\/en\/#organization\"},\"image\":{\"@id\":\"https:\/\/www.angulararchitects.io\/en\/blog\/custom-catalogs-in-a2ui-your-own-components-for-ai-generated-uis\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2026\/05\/shutterstock_2383749427.jpg\",\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\/\/www.angulararchitects.io\/en\/blog\/custom-catalogs-in-a2ui-your-own-components-for-ai-generated-uis\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/www.angulararchitects.io\/en\/blog\/custom-catalogs-in-a2ui-your-own-components-for-ai-generated-uis\/\",\"url\":\"https:\/\/www.angulararchitects.io\/en\/blog\/custom-catalogs-in-a2ui-your-own-components-for-ai-generated-uis\/\",\"name\":\"Custom Catalogs in A2UI: Your Own Components for AI-Generated UIs - ANGULARarchitects\",\"isPartOf\":{\"@id\":\"https:\/\/www.angulararchitects.io\/en\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/www.angulararchitects.io\/en\/blog\/custom-catalogs-in-a2ui-your-own-components-for-ai-generated-uis\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/www.angulararchitects.io\/en\/blog\/custom-catalogs-in-a2ui-your-own-components-for-ai-generated-uis\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2026\/05\/shutterstock_2383749427.jpg\",\"datePublished\":\"2026-05-01T19:34:53+00:00\",\"dateModified\":\"2026-07-30T06:01:42+00:00\",\"description\":\"How do you extend A2UI with your own domain-specific components? Using Angular as an example, this article shows how to define Custom Catalogs, secure them with Zod schemas, register them with the renderer, and wire them into the CopilotKit integration from the second part.\",\"breadcrumb\":{\"@id\":\"https:\/\/www.angulararchitects.io\/en\/blog\/custom-catalogs-in-a2ui-your-own-components-for-ai-generated-uis\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/www.angulararchitects.io\/en\/blog\/custom-catalogs-in-a2ui-your-own-components-for-ai-generated-uis\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/www.angulararchitects.io\/en\/blog\/custom-catalogs-in-a2ui-your-own-components-for-ai-generated-uis\/#primaryimage\",\"url\":\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2026\/05\/shutterstock_2383749427.jpg\",\"contentUrl\":\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2026\/05\/shutterstock_2383749427.jpg\",\"width\":1000,\"height\":667},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/www.angulararchitects.io\/en\/blog\/custom-catalogs-in-a2ui-your-own-components-for-ai-generated-uis\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/www.angulararchitects.io\/en\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Custom Catalogs in A2UI: Your Own Components for AI-Generated UIs\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/www.angulararchitects.io\/en\/#website\",\"url\":\"https:\/\/www.angulararchitects.io\/en\/\",\"name\":\"ANGULARarchitects\",\"description\":\"AngularArchitects.io\",\"publisher\":{\"@id\":\"https:\/\/www.angulararchitects.io\/en\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/www.angulararchitects.io\/en\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/www.angulararchitects.io\/en\/#organization\",\"name\":\"ANGULARarchitects\",\"alternateName\":\"SOFTWAREarchitects\",\"url\":\"https:\/\/www.angulararchitects.io\/en\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/www.angulararchitects.io\/en\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2023\/07\/AA-Logo-RGB-horizontal-inside-knowledge-black.svg\",\"contentUrl\":\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2023\/07\/AA-Logo-RGB-horizontal-inside-knowledge-black.svg\",\"width\":644,\"height\":216,\"caption\":\"ANGULARarchitects\"},\"image\":{\"@id\":\"https:\/\/www.angulararchitects.io\/en\/#\/schema\/logo\/image\/\"},\"sameAs\":[\"https:\/\/github.com\/angular-architects\",\"https:\/\/www.linkedin.com\/company\/angular-architects\/\"]},{\"@type\":\"Person\",\"@id\":\"https:\/\/www.angulararchitects.io\/en\/#\/schema\/person\/f3de69c1e2bdb5ba04d8d2f5f998b81a\",\"name\":\"Manfred Steyer\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/www.angulararchitects.io\/en\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/8778dfb353992fa3a0d909beee085a088891e5bfce65cdb3631801da527cf11b?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/8778dfb353992fa3a0d909beee085a088891e5bfce65cdb3631801da527cf11b?s=96&d=mm&r=g\",\"caption\":\"Manfred Steyer\"}}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Custom Catalogs in A2UI: Your Own Components for AI-Generated UIs - ANGULARarchitects","description":"How do you extend A2UI with your own domain-specific components? Using Angular as an example, this article shows how to define Custom Catalogs, secure them with Zod schemas, register them with the renderer, and wire them into the CopilotKit integration from the second part.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.angulararchitects.io\/en\/blog\/custom-catalogs-in-a2ui-your-own-components-for-ai-generated-uis\/","og_locale":"en_US","og_type":"article","og_title":"Custom Catalogs in A2UI: Your Own Components for AI-Generated UIs - ANGULARarchitects","og_description":"How do you extend A2UI with your own domain-specific components? Using Angular as an example, this article shows how to define Custom Catalogs, secure them with Zod schemas, register them with the renderer, and wire them into the CopilotKit integration from the second part.","og_url":"https:\/\/www.angulararchitects.io\/en\/blog\/custom-catalogs-in-a2ui-your-own-components-for-ai-generated-uis\/","og_site_name":"ANGULARarchitects","article_published_time":"2026-05-01T19:34:53+00:00","article_modified_time":"2026-07-30T06:01:42+00:00","og_image":[{"url":"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2026\/05\/series-1024x536.png","type":"","width":"","height":""}],"author":"Manfred Steyer","twitter_card":"summary_large_image","twitter_image":"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2026\/05\/series-1024x536.png","twitter_misc":{"Written by":"Manfred Steyer","Est. reading time":"20 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/www.angulararchitects.io\/en\/blog\/custom-catalogs-in-a2ui-your-own-components-for-ai-generated-uis\/#article","isPartOf":{"@id":"https:\/\/www.angulararchitects.io\/en\/blog\/custom-catalogs-in-a2ui-your-own-components-for-ai-generated-uis\/"},"author":{"name":"Manfred Steyer","@id":"https:\/\/www.angulararchitects.io\/en\/#\/schema\/person\/f3de69c1e2bdb5ba04d8d2f5f998b81a"},"headline":"Custom Catalogs in A2UI: Your Own Components for AI-Generated UIs","datePublished":"2026-05-01T19:34:53+00:00","dateModified":"2026-07-30T06:01:42+00:00","mainEntityOfPage":{"@id":"https:\/\/www.angulararchitects.io\/en\/blog\/custom-catalogs-in-a2ui-your-own-components-for-ai-generated-uis\/"},"wordCount":2778,"commentCount":0,"publisher":{"@id":"https:\/\/www.angulararchitects.io\/en\/#organization"},"image":{"@id":"https:\/\/www.angulararchitects.io\/en\/blog\/custom-catalogs-in-a2ui-your-own-components-for-ai-generated-uis\/#primaryimage"},"thumbnailUrl":"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2026\/05\/shutterstock_2383749427.jpg","inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/www.angulararchitects.io\/en\/blog\/custom-catalogs-in-a2ui-your-own-components-for-ai-generated-uis\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/www.angulararchitects.io\/en\/blog\/custom-catalogs-in-a2ui-your-own-components-for-ai-generated-uis\/","url":"https:\/\/www.angulararchitects.io\/en\/blog\/custom-catalogs-in-a2ui-your-own-components-for-ai-generated-uis\/","name":"Custom Catalogs in A2UI: Your Own Components for AI-Generated UIs - ANGULARarchitects","isPartOf":{"@id":"https:\/\/www.angulararchitects.io\/en\/#website"},"primaryImageOfPage":{"@id":"https:\/\/www.angulararchitects.io\/en\/blog\/custom-catalogs-in-a2ui-your-own-components-for-ai-generated-uis\/#primaryimage"},"image":{"@id":"https:\/\/www.angulararchitects.io\/en\/blog\/custom-catalogs-in-a2ui-your-own-components-for-ai-generated-uis\/#primaryimage"},"thumbnailUrl":"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2026\/05\/shutterstock_2383749427.jpg","datePublished":"2026-05-01T19:34:53+00:00","dateModified":"2026-07-30T06:01:42+00:00","description":"How do you extend A2UI with your own domain-specific components? Using Angular as an example, this article shows how to define Custom Catalogs, secure them with Zod schemas, register them with the renderer, and wire them into the CopilotKit integration from the second part.","breadcrumb":{"@id":"https:\/\/www.angulararchitects.io\/en\/blog\/custom-catalogs-in-a2ui-your-own-components-for-ai-generated-uis\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.angulararchitects.io\/en\/blog\/custom-catalogs-in-a2ui-your-own-components-for-ai-generated-uis\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.angulararchitects.io\/en\/blog\/custom-catalogs-in-a2ui-your-own-components-for-ai-generated-uis\/#primaryimage","url":"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2026\/05\/shutterstock_2383749427.jpg","contentUrl":"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2026\/05\/shutterstock_2383749427.jpg","width":1000,"height":667},{"@type":"BreadcrumbList","@id":"https:\/\/www.angulararchitects.io\/en\/blog\/custom-catalogs-in-a2ui-your-own-components-for-ai-generated-uis\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/www.angulararchitects.io\/en\/"},{"@type":"ListItem","position":2,"name":"Custom Catalogs in A2UI: Your Own Components for AI-Generated UIs"}]},{"@type":"WebSite","@id":"https:\/\/www.angulararchitects.io\/en\/#website","url":"https:\/\/www.angulararchitects.io\/en\/","name":"ANGULARarchitects","description":"AngularArchitects.io","publisher":{"@id":"https:\/\/www.angulararchitects.io\/en\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.angulararchitects.io\/en\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/www.angulararchitects.io\/en\/#organization","name":"ANGULARarchitects","alternateName":"SOFTWAREarchitects","url":"https:\/\/www.angulararchitects.io\/en\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.angulararchitects.io\/en\/#\/schema\/logo\/image\/","url":"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2023\/07\/AA-Logo-RGB-horizontal-inside-knowledge-black.svg","contentUrl":"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2023\/07\/AA-Logo-RGB-horizontal-inside-knowledge-black.svg","width":644,"height":216,"caption":"ANGULARarchitects"},"image":{"@id":"https:\/\/www.angulararchitects.io\/en\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/github.com\/angular-architects","https:\/\/www.linkedin.com\/company\/angular-architects\/"]},{"@type":"Person","@id":"https:\/\/www.angulararchitects.io\/en\/#\/schema\/person\/f3de69c1e2bdb5ba04d8d2f5f998b81a","name":"Manfred Steyer","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.angulararchitects.io\/en\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/8778dfb353992fa3a0d909beee085a088891e5bfce65cdb3631801da527cf11b?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/8778dfb353992fa3a0d909beee085a088891e5bfce65cdb3631801da527cf11b?s=96&d=mm&r=g","caption":"Manfred Steyer"}}]}},"_links":{"self":[{"href":"https:\/\/www.angulararchitects.io\/en\/wp-json\/wp\/v2\/posts\/33332","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.angulararchitects.io\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.angulararchitects.io\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.angulararchitects.io\/en\/wp-json\/wp\/v2\/users\/25"}],"replies":[{"embeddable":true,"href":"https:\/\/www.angulararchitects.io\/en\/wp-json\/wp\/v2\/comments?post=33332"}],"version-history":[{"count":7,"href":"https:\/\/www.angulararchitects.io\/en\/wp-json\/wp\/v2\/posts\/33332\/revisions"}],"predecessor-version":[{"id":34536,"href":"https:\/\/www.angulararchitects.io\/en\/wp-json\/wp\/v2\/posts\/33332\/revisions\/34536"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.angulararchitects.io\/en\/wp-json\/wp\/v2\/media\/33334"}],"wp:attachment":[{"href":"https:\/\/www.angulararchitects.io\/en\/wp-json\/wp\/v2\/media?parent=33332"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.angulararchitects.io\/en\/wp-json\/wp\/v2\/categories?post=33332"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.angulararchitects.io\/en\/wp-json\/wp\/v2\/tags?post=33332"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}