RFC-155: Agentic and MCP UI for Forge

Following the release of Forge Rovo agents, Rovo MCP, and Rovo actions, we’re exploring how your Forge agents and tools can present interactive UI on Atlassian Rovo surfaces (e.g. Rovo Chat) and in external AI experiences.

This RFC seeks early feedback on a proposed Early Access Program (EAP) for A2UI components in Rovo actions via Rovo Agents rendered in Rovo Chat. It also outlines our longer-term direction: making those components available through Rovo MCP (Preview) in external AI chats, alongside support for custom UI MCP-apps through rovo:mcp. These future integrations are currently planned but scope and implementation is yet to be aligned.

The ask: We’d like your feedback on the proposed API, any questions it raises, and whether it supports the experiences you want to build for your tools and agents. Share your use cases and anything you’d need that the proposal doesn’t yet cover.

Milestone 1: A2UI High-Level Components rendered in Rovo Chat

Milestone 1 introduces high-level A2UI components that your Rovo actions can return and render in Rovo Chat. This RFC covers the proposed API and interaction model, with an example component catalogue for the Early Access Program to follow.

The candidate EAP components are:
ChoiceList: Select from bounded options with descriptions, with explicit submission.


ConfirmationPanel: Request explicit confirmation with a title, summary, and confirm button.

ResultPanel: Display a read-only outcome with title and summary. Best used after an agent action has been completed

Analytical Chart: Visualise distributions and trends with percentiles, summary, and data access.


Explore more upcoming components and vote here:https://forge-a2ui-catalogue.kaizen.shared.atlassian-3p.com/

Future Plans: Exposing A2UI and custom UI via MCP-apps in rovo:mcp

Our goal is to let you build an interactive Forge action once and reuse it across Rovo Chat and supported external AI chats. We plan to make the same A2UI components available through rovo:mcp, so connecting your action to a Rovo agent or exposing it as an MCP tool would bring its UI with it, without requiring a separate frontend for each supported client.

For experiences that need bespoke layouts or more complex workflows, we also plan to support Custom UI through MCP Apps in rovo:mcp. This would give you the flexibility to build app-like experiences within chat, alongside the reusable A2UI components.

These plans are beyond the EAP scope and client compatibility and the supported interaction model are still being defined.

See Build a custom frontend below for the proposed MCP-app developer flow.

Choosing between A2UI prebuilt components and custom MCP-apps

Consideration Prebuilt A2UI components Resource frontend MCP-apps
What it offers 1. Consistent end-user experience in chat with Atlassian agents 2. Create once and will surface on both internal and external chat surfaces (Future plan) 3. Simple to use 1. Creates custom UI experiences in Chat which may not exist in A2UI components 2. Enables re-use of existing frontend code
What you write A component name, its props (display settings and data), and the operation to call for each event. Your frontend code, its local state and calls to backend operations.
What ui contains { component, props, events? }, checked against the declared catalog. Your app’s data. It does not describe a required component tree.
Interaction The component collects submitted values and calls the operation you specified. Your frontend calls invokeOperation(name, { input }).
Limits One component can be rendered per view, limited customisation. Support depends on the host’s resource rendering and available bridge features.

Example code for A2UI pre-built components and custom ui in MCP-apps

In both examples below, a user approves expenses in Rovo Chat: they select expenses, review the total, confirm approval and see the result. The Forge action uses a prebuilt component for each step. Your app checks permissions and handles the approval, while Forge displays the UI.

Build with prebuilt components

1. Find a component and read its contract

The sample component reference defines ChoiceList, ConfirmationPanel and ResultPanel, with mockups, props and events. These are proposed examples, not implemented catalog entries. That section includes their prop types, defaults, supported events and submitted-value shapes. The code below uses that same contract; you do not need to work with raw A2UI messages or schemas.

For example, a generic ChoiceList can present expenses, issue priorities or project choices. Its options have IDs, labels and optional descriptions; the component itself knows nothing about expenses. ChoiceList.props.selectedIds sets the initial selection. Its onSubmit event supplies values.selectedIds. The reference needs to explain both so you can connect a component to a handler without guessing.

2. Declare the tool’s output

This manifest excerpt uses the proposed outputs configuration. Standard app metadata, permissions and agent/MCP bindings are omitted. The handler import paths later in this document are also proposed.

modules:
  action:
    - key: quick-review-expenses
      name: Review expenses
      description: Select expenses, review the total, and confirm approval.
      function: quick-review
      actionVerb: UPDATE # Reachable operations include an approval write.
      inputs:
        reportId:
          title: Report ID
          type: string
          required: true
          description: The expense report to review.
      outputs:
        text: false # Completing this particular task requires UI.
        ui:
          type: prebuilt
          catalog: forge-standard@1 # Illustrative catalog identifier.
          components: [ChoiceList, ConfirmationPanel, ResultPanel]
  function:
    - key: quick-review
      handler: prebuilt.handler

The function entry connects quick-review to the exported handler in src/prebuilt.js.

The components list names the components this tool is allowed to show. Each successful response chooses one. A later operation can switch to another, such as replacing the selection form with a confirmation panel. You define the operations in JavaScript, without repeating them in the manifest.

3. Implement the initial handler and operations

The following is the complete UI-handling example for prebuilt.js. The imported business services are app code; their responsibilities are described below. The sample component reference defines every component prop and event used here.

Full prebuilt handler: prebuilt.js
// Proposed SDK, not an installed Forge API. App business services are omitted.
import { defineTool } from '@forge/api/agentic-ui';
import { loadReport, prepareApproval, commitApproval } from './expenses.js';

function selection(report, selectedIds = [], errorMessage) {
  return {
    component: 'ChoiceList',
    props: {
      question: 'Review expenses',
      selection: 'multiple',
      options: report.expenses.map(expense => ({
        id: expense.id, label: expense.description, description: expense.amountLabel,
      })),
      selectedIds,
      submitLabel: 'Review selected',
      ...(errorMessage ? { errorMessage } : {}),
    },
    events: {
      onSubmit: { operation: 'review', input: { reportId: report.id } },
    },
  };
}

export const handler = defineTool({
  async run(input, forgeContext) {
    // loadReport validates reportId and checks the current user's access.
    const report = await loadReport(input.reportId, forgeContext);
    return {
      text: `Report ${report.id}: ${report.expenses.length} expenses awaiting selection.`,
      ui: selection(report),
    };
  },
  operations: {
    async review({ input, values }, forgeContext) {
      const report = await loadReport(input.reportId, forgeContext);
      const ids = values?.selectedIds; // ChoiceList IDs identify expenses in this app.
      const allowed = new Set(report.expenses.map(expense => expense.id));
      if (!Array.isArray(ids) || ids.length === 0 ||
          ids.some(id => typeof id !== 'string' || !allowed.has(id)) ||
          new Set(ids).size !== ids.length) {
        const message = 'Select at least one available expense.';
        return {
          text: message,
          ui: selection(report, [], message), // Complete correction view.
        };
      }
      // Rechecks eligibility, calculates totals on the backend and stores
      // a prepared review bound to this user and report. No approval yet.
      const review = await prepareApproval(report.id, ids, forgeContext);
      return {
        text: review.summary,
        ui: {
          component: 'ConfirmationPanel',
          props: {
            title: 'Confirm approval',
            summary: review.summary,
            confirmLabel: 'Approve',
          },
          events: {
            onConfirm: {
              operation: 'approve',
              input: { reportId: report.id, reviewId: review.id },
            },
          },
        },
      };
    },
    async approve({ input }, forgeContext) {
      // Rechecks authorization and freshness. Repeated commits of the same
      // review must return its recorded outcome, without approving twice.
      const result = await commitApproval(input, forgeContext);
      return {
        text: result.summary,
        ui: {
          component: 'ResultPanel',
          props: { title: 'Expenses approved', summary: result.summary },
        },
        completed: true,
      };
    },
  },
});

In the event configuration, input carries data your app already knows, such as the report ID. values carries the user’s submitted selection, collected by the component. For the selection event, the review operation receives:

{ input: { reportId: 'EXP-42' }, values: { selectedIds: ['train', 'hotel'] } }

forgeContext arrives separately and contains trusted Forge execution context. Your backend must still check whether the user can access the report and approve those expenses. An ID supplied by the UI is not permission to act on it.

The sample ChoiceList returns values.selectedIds. The review handler treats those IDs as expenses because this app created the options from its report. The component does not impose that business meaning.

Every successful prebuilt call returns everything needed to show the next component: its name, props and any events. A correction returns the selection component with an error message; success replaces it with confirmation. The final result has no event bindings and declares completed: true. Include the events needed by each new view; events from the previous view are not kept automatically.

Build a custom frontend

Use a resource frontend when you need your own layout, controls or frontend framework. This builds on Forge resources and Custom UI. It uses the same defineTool, operations and result fields. Its ui is app-owned data, not a prebuilt component description.

1. Declare a resource output

Both example tools are entries in the same modules.action list. They use the same action, input and function configuration. Their UI output settings select either prebuilt components or a frontend resource. Choose whichever tools your app needs; both are shown here for comparison. “Quick” and “detailed” are example app names, not different APIs or fixed internal/external categories.

Setting Prebuilt example Custom frontend example
Action key under modules.action quick-review-expenses detailed-review-expenses
Input reportId reportId
Action verb UPDATE UPDATE
Function key quick-review detailed-review
Handler prebuilt.handler resource.handler
Proposed outputs.ui.type prebuilt resource
UI settings catalog and allowed components A resource key referencing the frontend
Backend API defineTool({ run, operations }) defineTool({ run, operations })

The manifest below connects both actions to their functions and UI configuration. It also shows agent/MCP lists referring to those same action keys. Standard app metadata and permissions are omitted. The outputs presentation settings are proposed additions.

modules:
  rovo:agent:
    - key: expense-assistant
      name: Expense assistant
      prompt: Help users review expense reports using quick-review-expenses.
      actions: [quick-review-expenses] # Refers to an action below.

  rovo:mcp:
    - key: expense-tools
      name: Expense tools
      tools: [detailed-review-expenses] # Refers to an action below.

  action: # Both UI approaches use this same list of tool definitions.
    - key: quick-review-expenses
      name: Quick expense review
      description: Select expenses, review the total, and confirm approval.
      function: quick-review
      actionVerb: UPDATE
      inputs:
        reportId:
          title: Report ID
          type: string
          required: true
          description: The expense report to review.
      outputs: # Proposed presentation settings.
        text: false
        ui:
          type: prebuilt
          catalog: forge-standard@1
          components: [ChoiceList, ConfirmationPanel, ResultPanel]

    - key: detailed-review-expenses
      name: Detailed expense review
      description: Review and approve expenses through the app's custom frontend.
      function: detailed-review
      actionVerb: UPDATE
      inputs:
        reportId:
          title: Report ID
          type: string
          required: true
          description: The expense report to review.
      outputs: # Same output structure; a different UI approach.
        text: false
        ui:
          type: resource
          resource: expense-review-ui # Matches resources.key below.

  function:
    - key: quick-review
      handler: prebuilt.handler # Exported handler in src/prebuilt.js.
    - key: detailed-review
      handler: resource.handler # Exported handler in src/resource.js.

resources:
  - key: expense-review-ui
    path: static/expense-review/dist # Built frontend, including index.html.

detailed-review-expenses.function selects the backend function. Its outputs.ui.resource selects the frontend. The top-level resources entry identifies the frontend files; it does not define another action. The binding arrangement above is an example: either tool can be connected to another integration when that integration supports its UI approach.

2. Return app-owned data

The full resource handler reuses the same business services. Its run returns:

{
  text: 'Report EXP-42: 2 expenses awaiting selection.',
  ui: {
    step: 'selection', // An app-defined state, not a Forge enum.
    report: {
      id: 'EXP-42',
      expenses: [
        { id: 'train', description: 'Train ticket', amountLabel: 'AUD 30.00' },
        { id: 'hotel', description: 'Hotel', amountLabel: 'AUD 90.00' },
      ],
    },
    selectedIds: [],
  },
}

Its review operation reads input.expenseIds, rather than host-collected values. After preparing a review, it returns:

{
  text: 'Approve 2 expenses totalling AUD 120.00?',
  ui: {
    step: 'confirmation', reportId: 'EXP-42', reviewId: 'review-7',
    summary: 'Approve 2 expenses totalling AUD 120.00?',
  },
}

Approval returns ui: { step: 'result', summary } and completed: true. These step, report and summary fields are defined by the app; Forge does not require this state model.

3. Read context and call operations

The complete React example renders those three app-defined views. The bridge calls at its center are:

import { getContext, invokeOperation } from '@forge/bridge/agentic-ui';

const initial = await getContext(); // Initial data for this mounted UI instance.
// Render initial.ui and honor initial.completed.

const review = await invokeOperation('review', {
  input: { reportId: 'EXP-42', expenseIds: ['train', 'hotel'] },
});
// Render review.ui. The call does not itself approve anything.

// On a later, separate user confirmation:
const approval = await invokeOperation('approve', {
  input: { reportId: review.ui.reportId, reviewId: review.ui.reviewId },
});
// Render approval.ui and disable interaction when approval.completed is true.

This excerpt shows the call sequence; the complete frontend includes loading, error and pending states. It prevents duplicate clicks in that view. The backend still handles repeated calls or calls from other views. Unsubmitted checkbox edits stay in frontend state and are not automatically sent to the agent.

Where can the tool appear?

The Rovo Agent and Rovo MCP (Preview) modules select where actions are available. They refer to the action keys from the combined manifest; they do not define separate prebuilt and custom-frontend action types.

Manifest section What it does in this example
modules.action Defines both quick-review-expenses and detailed-review-expenses.
rovo:agent.actions Lets the expense agent call the listed action.
rovo:mcp.tools Exposes the listed action as an MCP tool.

The proposal aims to let you reuse either UI approach across supported integrations. The example is not a support matrix or a promise that every client can display either UI today. An interactive tool needs rendering, operation calls and any required agent-context updates. If the app can complete the task through text, it can use that approach when UI is unavailable. If the task requires UI, the tool should only be offered where that UI can work.

Results, text and completion

Result field Meaning
text A string or JSON object for the agent and conversation record, describing the same facts and status as the UI.
ui The next component and its settings for prebuilt UI; data your own frontend understands for resource UI.
completed true declares that this UI experience is finished. Omitted or false does not declare completion.

outputs.text says whether the task can be completed through text; it does not set the type of the returned text field. It defaults to true: the app can finish the task without UI. Set it to false when the task requires interaction with UI. Either way, meaningful outcomes still need text so the conversation has an understandable record. For the examples here, the history must distinguish “awaiting selection,” “awaiting confirmation” and “approved”.

On supported integrations, operation text updates the agent’s context without requesting another model turn. A frontend edit alone does not do this. JSON text must preserve the same facts when shown as text; you should not rely on the model to reconstruct missing approval outcomes.

completed: true should retain the final view and stop further interaction; it does not request removal of the UI. External clients may keep older views active, so backend authorization and current-state checks still matter. Starting the tool again creates a new UI instance.

For prebuilt UI, omitting ui is not a supported successful response in this proposal. For resource UI, an omitted ui means no replacement data; the frontend keeps its current view. Omission does not mean completion. Read-only tools may return their final view and completed: true immediately.

Validation and failures

Return an ordinary result with a correction view when the user can fix a business input, as the empty-selection example does. Unexpected app or platform failures reject the frontend call with an error. Do not assume a timed-out approval failed to commit; check the report before retrying. The proposal does not automatically queue or retry your writes.

For prebuilt components, the host renders the complete response using the component contract. Your resource frontend renders its own pending, correction and failure states. Neither approach removes the need for backend freshness and repeat-call checks.

Compatibility and initial limits

The catalog name includes a version, illustrated by forge-standard@1. Changes that keep existing components working can stay in that version. If a component changes in a way that requires you to update your app, you must explicitly choose a new catalog version. Deploying your own frontend is separate from upgrading a component catalog.

The first version focuses on submitting a form or clicking a confirmation button. It does not define a backend call for every keystroke or checkbox change, combining prebuilt components into a custom layout, restoring every unsaved frontend edit, or using Forge Remote as the direct handler. Your own frontend can still support local interactions without calling the backend.

What the design should achieve

Alongside your specific use cases, please consider whether the examples achieve these goals:

Goal What to consider
Build without learning two different APIs Can you understand both approaches and share business logic where useful?
Choose when to upgrade Is it clear how component upgrades differ from deploying your own frontend?
Keep the conversation understandable Does the text explain what is waiting for input and what has actually happened?
Use Forge permissions consistently Is it clear which checks your backend still needs to perform?
Make completion clear Can users tell when the experience has finished and what to do when returning later?

Questions for partners

  1. What experience would you build, and which UI approach would you choose? What would you need that is missing here?
  2. After reading the sample component reference, is it clear how to connect a component to an operation?
  3. Did you have any other feedback on the API or proposed developer experience?
Appendix: terms
Term Meaning
Tool A Forge action made available to an AI agent.
Operation A UI-invoked backend function belonging to that tool.
UI instance The experience produced by one tool invocation and its later updates.
Catalog A versioned collection of component contracts supplied by the platform.
Resource frontend Your app’s frontend bundle, selected by a Forge resource key.
Surface The client displaying the conversation, such as Rovo Chat or Claude Desktop.
Appendix: sample component reference

Proposal for partner review. These reusable components and their exact schemas are documentation examples, not available Forge catalog entries. They make the API walkthrough concrete. ChoiceList uses a candidate catalog name, but the precise props and event values below are proposed for this walkthrough. The catalog identifier forge-standard@1 is also proposed; no availability on a particular surface is implied.

The walkthrough shows how to use these components. The tables and event examples below define the proposed developer-facing contract. Forge handles the client-specific UI format. Your backend must still validate submitted data and authorize every operation.

Supply unique option IDs, ensure selected IDs refer to existing options, and select at most one ID in single-selection mode. Every enabled interactive component needs its event binding. These rules apply alongside the prop types below.

ChoiceList

Use this component when a user needs to choose one or more options: issue priorities, projects, candidate proposals or expenses. It renders a question and options with labels and optional descriptions. Single selection uses radio buttons; multiple selection uses checkboxes. The app decides what each option ID means and what submitting it does.

Prop Type Required / default Meaning
question string Required The question or heading shown above the options.
selection "single" or "multiple" Required Whether the user may choose one or several options.
options array of { id, label, description? } Required Available options. Each field is a string; IDs must be unique. Descriptions are optional supporting text.
selectedIds string array Required Initially selected option IDs. IDs must exist in options; single-selection mode permits at most one. A correction response can restore valid selections.
submitLabel string Required Label for the submit button.
errorMessage string Optional; no error Correction message above the submit button.
disabled boolean Optional; false Disables selection and submission while retaining visible content.

Event: onSubmit

For this expense example, the app maps expense.id to option id, the expense description to label, and the formatted amount to option description. None of those expense concepts is a component prop.

Bind onSubmit under ui.events. The host collects the current selection and calls the bound operation. Submitting without selecting anything produces an empty array; the backend decides whether that is valid.

events: {
  onSubmit: {
    operation: 'review',
    input: { reportId: 'EXP-42' }, // App-supplied context for this view.
  },
}

The operation receives:

{
  input: { reportId: 'EXP-42' },
  values: { selectedIds: ['train', 'hotel'] }, // Current selection, collected by host.
}

selectedIds configures the displayed selection; values.selectedIds reports the selection at submission. They have different roles. An empty selection is represented as selectedIds: [], in either mode. This sample component has no per-option backend event. Unsubmitted edits stay local to the view. An enabled, interactive selection view needs an onSubmit binding.

The same component for a different task

For an issue-priority picker, use the same component contract with different options and an app-defined operation:

{
  component: 'ChoiceList',
  props: {
    question: 'Which priority should this issue have?',
    selection: 'single',
    options: [
      { id: 'high', label: 'High', description: 'Needs attention soon' },
      { id: 'medium', label: 'Medium' },
    ],
    selectedIds: ['medium'],
    submitLabel: 'Set priority',
  },
  events: {
    onSubmit: { operation: 'setPriority', input: { issueKey: 'PROJ-1' } },
  },
}

Selecting High supplies values: { selectedIds: ['high'] }. The setPriority operation maps that option ID to the app’s priority value and checks permission to update the issue. There are no expense-specific props or events.

ConfirmationPanel

Use this component to show the backend’s prepared approval summary before the user commits it.

Prop Type Required / default Meaning
title string Required Heading for the confirmation step.
summary string Required Plain-text description of what will be approved.
confirmLabel string Required Label for the confirmation button.
errorMessage string Optional; no error A business correction message, if the app can safely continue.
disabled boolean Optional; false Disables confirmation and retains the summary.

Event: onConfirm

events: {
  onConfirm: {
    operation: 'approve',
    input: { reportId: 'EXP-42', reviewId: 'review-7' },
  },
}

The operation receives input with those fields and no form values. The backend loads the prepared review and verifies it is still valid and authorized. A review ID does not grant permission. An enabled confirmation view needs an onConfirm binding.

ResultPanel

Use this read-only component to show the final outcome.

Prop Type Required Meaning
title string Yes Outcome heading.
summary string Yes Plain-text outcome, including relevant facts.

ResultPanel has no events or disabled prop because it has no interactive controls. The tool result sets completed: true separately to declare the experience complete. Component appearance alone does not define completion across UI types.

Version and surface support

The walkthrough declares these example components under forge-standard@1. Compatible additions can remain in that catalog version; a breaking contract change requires an explicit version upgrade. A resource frontend is versioned with the app’s deployed frontend, separately from this catalog.

The proposal aims to support both Rovo Chat and compatible external clients. This sample reference does not establish which components are implemented on either. Published component documentation would need a verified support table before developers could rely on it.

Appendix: resource handler

This is resource.js, using the app-owned business services described in the walkthrough. The returned ui contains app data, with no catalog component names or props.

// Proposed SDK, not an installed Forge API. Reuses the prebuilt example's
// app-owned business services; this file is a separate tool handler.
import { defineTool } from '@forge/api/agentic-ui';
import { loadReport, prepareApproval, commitApproval } from './expenses.js';

function selection(report, errorMessage) {
  return { step: 'selection', report, selectedIds: [],
    ...(errorMessage ? { errorMessage } : {}) };
}

export const handler = defineTool({
  async run(input, forgeContext) {
    const report = await loadReport(input.reportId, forgeContext);
    return {
      text: `Report ${report.id}: ${report.expenses.length} expenses awaiting selection.`,
      ui: selection(report), // App data, not a catalog component description.
    };
  },
  operations: {
    async review({ input }, forgeContext) {
      const report = await loadReport(input.reportId, forgeContext);
      const ids = input.expenseIds;
      const allowed = new Set(report.expenses.map(expense => expense.id));
      if (!Array.isArray(ids) || ids.length === 0 ||
          ids.some(id => typeof id !== 'string' || !allowed.has(id)) ||
          new Set(ids).size !== ids.length) {
        const message = 'Select at least one available expense.';
        return { text: message, ui: selection(report, message) };
      }
      const review = await prepareApproval(report.id, ids, forgeContext);
      return {
        text: review.summary,
        ui: { step: 'confirmation', reportId: report.id, reviewId: review.id,
          summary: review.summary },
      };
    },
    async approve({ input }, forgeContext) {
      const result = await commitApproval(input, forgeContext);
      return {
        text: result.summary,
        ui: { step: 'result', summary: result.summary },
        completed: true,
      };
    },
  },
});
Appendix: resource frontend

This React component renders the app-defined selection, confirmation and result views from the resource handler. Mount it in your resource frontend using your normal React entry point. It includes loading, pending and error states; the bridge entry point remains proposed.

import React, { useEffect, useRef, useState } from 'react';
import { getContext, invokeOperation } from '@forge/bridge/agentic-ui';

export function ExpenseReview() {
  const [view, setView] = useState(null);
  const [selectedIds, setSelectedIds] = useState([]);
  const [completed, setCompleted] = useState(false);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState(null);
  const pending = useRef(false);

  useEffect(() => {
    let active = true;
    getContext().then(result => {
      if (!active) return;
      setView(result.ui);
      setSelectedIds(result.ui?.selectedIds ?? []);
      setCompleted(result.completed === true);
    }).catch(() => {
      if (active) setError('Could not load this view. Check the conversation result.');
    });
    return () => { active = false; };
  }, []);

  async function call(name, input) {
    if (pending.current || completed) return;
    pending.current = true;
    setBusy(true);
    setError(null);
    try {
      const result = await invokeOperation(name, { input });
      if (result.ui !== undefined) {
        setView(result.ui);
        setSelectedIds(result.ui.selectedIds ?? []);
      }
      if (result.completed === true) setCompleted(true);
    } catch {
      // A timeout or failure does not prove that a write did not happen.
      setError('Could not confirm the outcome. Check the report before retrying.');
    } finally {
      pending.current = false;
      setBusy(false);
    }
  }

  if (!view) return <p role="status">{error ?? 'Loading expenses…'}</p>;
  return <section aria-busy={busy}>
    {error && <p role="alert">{error}</p>}
    {view.step === 'selection' && <>
      <h2>Review expenses</h2>
      {view.errorMessage && <p role="alert">{view.errorMessage}</p>}
      <fieldset disabled={busy || completed}>
        <legend>Select expenses to review</legend>
        {view.report.expenses.map(expense => <label key={expense.id}>
          <input type="checkbox"
            checked={selectedIds.includes(expense.id)}
            onChange={event => setSelectedIds(ids => event.target.checked
              ? [...ids, expense.id] : ids.filter(id => id !== expense.id))} />
          {expense.description} — {expense.amountLabel}
        </label>)}
      </fieldset>
      <button disabled={busy || completed}
        onClick={() => call('review', {
          reportId: view.report.id, expenseIds: selectedIds,
        })}>Review selected</button>
    </>}
    {view.step === 'confirmation' && <>
      <h2>Confirm approval</h2>
      <p>{view.summary}</p>
      <button disabled={busy || completed}
        onClick={() => call('approve', {
          reportId: view.reportId, reviewId: view.reviewId,
        })}>Approve</button>
    </>}
    {view.step === 'result' && <>
      <h2>Expenses approved</h2>
      <p>{view.summary}</p>
    </>}
  </section>;
}

@VickyHu thanks for the proposal. Still processing it - meanwhile, it looks like the link to https://forge-a2ui-catalogue.kaizen.shared.atlassian-3p.com/ is only returning Site not available.

Thanks for letting me know Ben, I’ve replaced it with static image examples!

Hi Vicky,

Thanks for sharing this proposal. The ability to combine structured data operations with interactive UI inside Rovo is particularly interesting.

One use case we’re exploring involves working with structured records across Jira and Confluence: searching and filtering records, selecting multiple entries, editing values, and confirming changes before they’re committed.

For these scenarios, a simple ChoiceList might not be sufficient. We’d potentially need paginated tables, dynamic forms, field validation, and contextual permissions.

Would these more advanced data interactions be expected to use Custom UI resources rather than the proposed A2UI components?

I’m also curious whether the longer-term goal is to support the same interactive experience across Rovo Chat and external MCP clients without maintaining separate frontends.

Thanks!

Hey Mia,

Thanks for sharing your use case!

Although forms and data tables aren’t part of the initial EAP, we’re exploring richer interactions for the A2UI catalogue. Our goal is to cover common workflows so they’re easy to complete within chat. For bespoke layouts, more complex interactions or brand-specific experiences, Custom UI through MCP Apps is the proposed alternative.

I’ve included some early form and data table designs below. These aren’t finalised or committed to a release, but they illustrate the direction we’re exploring.

Could you share which input types and validation rules you need? It would also help to know whether validation needs to happen during editing or whether checking on submit would be sufficient.

Your backend remains responsible for checking contextual permissions on each operation. In the proposed API, forgeContext supplies the trusted execution context for those checks.

And to your last question, that’s our long-term goal for A2UI. We’re planning to make the same component definitions work in Rovo Chat and supported external MCP clients through MCP Apps, so you wouldn’t need to maintain a separate frontend for each client. The aim is to preserve the same interactive workflow, though presentation may vary by host.



Hey everyone!
We’ve made our component exploration site public for review. You’re able to vote on the components you think are most useful for your use cases.

Let me know if there’s link access issues here.