# Convert Ui5 To Fiori Elements

> Generate a Fiori Elements V4 LROP app (list report + object page) driven by @UI.* annotations on a V4 RAP service, using the Fiori MCP server's 3-step (list_functionalities → get_functionality_details → execute_functionality) workflow. Use when asked to "build a Fiori Elements app", "generate LROP from this V4 service", "convert to annotation-driven UI", or "scaffold Fiori Elements V4".

- Skill: `arc-mcp/convert-ui5-to-fiori-elements` (Agent Skill)
- Install (CLI): `npx skillmds@latest add arc-mcp/convert-ui5-to-fiori-elements`
- Raw SKILL.md: https://api.skillmd.com/api/skills/arc-mcp/convert-ui5-to-fiori-elements/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: arc-mcp (https://skillmd.com/u/arc-mcp)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/arc-mcp/convert-ui5-to-fiori-elements

---


# Convert legacy UI5 JS app ➜ Fiori Elements V4 (annotation-first)

Generate a Fiori Elements V4 list-report + object-page app driven by `@UI.*` annotations on
the RAP CDS projection. Backend annotations are derived from the **legacy** UI5 app's actual
features (columns, search, sort, formatters, action buttons, tab structure) so the FE app
reproduces the user-visible contract. Custom behavior FE templates can't express is wired via
the extension API.

This skill is **one of two parallel UI paths** after the RAP backend lands. Pick this one if
the target architecture is **Fiori Elements V4** (annotation-driven; minimal custom code). Pick
`modernize-ui5-app.md` instead if the target is **freestyle TypeScript** (custom controllers,
manual binding). Both start from the same legacy JS app + the same V4 RAP service.

```
                  migrate-segw-to-rap.md  (backend: SEGW V2 → RAP V4)
                            │
              ┌─────────────┴─────────────┐
              ▼                           ▼
    modernize-ui5-app.md      convert-ui5-to-fiori-elements.md
        (freestyle TS)               (Fiori Elements V4)
```

This skill depends on:

- `migrate-segw-to-rap.md` having produced an active V4 RAP service (CDS roots + projections,
  BDEF, SRVD, SRVB published, V4 routing group registered in `/n/IWFND/MAINT_SERVICE`).
- The legacy app (`<source_app>/`) still being readable — Phase 1 mines its features even
  though the legacy app itself is not modified.

> **Independent of `modernize-ui5-app.md`.** This skill does not require the modern TS app to
> exist. The legacy app is the single source of truth for user-visible features; the FE app
> reproduces them through annotations + extensions directly.

> **Domain example.** Annotation templates use an illustrative `Project → Tasks → TimeEntries`
> domain. Substitute the user's entities throughout — the LLM rewrites every projection /
> entity / field identifier to match the V4 service.

> **Canonical annotation reference.** This skill points at the **ABAP RAP Fiori Feature
> Showcase** (`SAP-samples/abap-platform-fiori-feature-showcase`) as the authoritative source
> for `@UI.*` / `@ObjectModel.*` / `@Common.*` annotation patterns. When uncertain about how
> to express a legacy UI feature as an annotation, search the showcase via
> `mcp__sap-docs__search` with the feature's *Search Term* (e.g. `#OPHeaderAction`,
> `#LineItemHighlight`, `#HeaderInfo`, `#ActionInLineItem`). The showcase is the
> ground-truth catalog every annotation in this skill maps back to.

---

## Using `mcp__sap-docs__search` for annotation discovery (read this first)

This skill names a handful of canonical annotation patterns inline. **Treat them as
starting points, not the whole catalog.** Anything not explicitly covered — alternate
visual representations, dynamic feature control, charts, value helps, semantic keys,
multi-level OP routing, draft-specific UI behavior, or 7.58-specific syntax issues —
**look up in `mcp__sap-docs__search` before guessing**. The MCP indexes the
authoritative SAP-maintained references, has up-to-date showcase tags, and includes
release-specific documentation (758 news, cloud differences, etc.) that this skill text
can't keep current with on its own.

### Canonical libraries to know

| Library ID | What's in it |
|---|---|
| `/abap-fiori-showcase` | **SAP-samples/abap-platform-fiori-feature-showcase** — the on-prem RAP + FE V4 catalog. Every pattern is tagged with a Search Term like `#HeaderInfo`, `#LineItemHighlight`, `#OPHeaderAction`, `#OPTable`, `#DataFieldForAction`, `#SemanticKey`. The 7.58-accurate baseline for this skill. |
| `/cap-fiori-showcase` | **SAP-samples/fiori-elements-feature-showcase** — CAP/CDS V4 counterpart. Same conceptual patterns from the CAP side; useful when the ABAP showcase tag doesn't include the variant you need, especially for FE manifest layout (`layouts_ChildEntities*.cds`, `#Subpages`, `#InboundNav`). |
| `/abap-docs-standard` | Official ABAP keyword docs (on-prem). Definitive for syntax (`FOR BEHAVIOR OF`, virtual elements, projection BDEF rules), release-specific capabilities (`ABENNEWS-758-RESTFUL` for what's available on 7.58), and the deep theory behind any RAP construct. |
| `/abap-docs-cloud` | Same as above for cloud/BTP. Useful when something looks cloud-only — verify before pulling into on-prem 7.58. |
| `/sap-help` (online) | SAP Help full content. Best for SAPUI5 manifest configuration, FE template runtime details, *Configuring Internal Navigation*. Requires `includeOnline=true`. |
| `/abap-cheat-sheets` | Practical RAP / ABAP snippets with current syntax variants. |

### Search recipe — apply for every annotation question

1. **Identify what the user sees**: "status as a colored chip", "click row → child detail page", "filter with fixed values".
2. **Search the showcase by hashtag** — most patterns are tagged literally in markdown:
   ```
   mcp__sap-docs__search(query="#TagName")           // exact tag lookup
   mcp__sap-docs__search(query="<visual description>")  // semantic fallback
   ```
3. **Fetch the full pattern** of the most relevant hit:
   ```
   mcp__sap-docs__fetch(id="/abap-fiori-showcase/<section>#<tag>")
   ```
4. **Adapt to the user's entities** — the showcase uses `/DMO/FSA_R_RootTP` etc.; substitute the user's projection names.
5. **If the on-prem pattern is rejected on the target release**, search again on a different axis:
   - `/abap-docs-standard/ABENCDS_PROJ_VIEW_VIRTUAL_ELEMENT` (+ example `ABENCDS_PROJ_VIEW_VIRTEL_ABEXA`) for virtual elements with ABAP-class calculation
   - `/abap-docs-standard/ABENNEWS-<release>-RESTFUL` for release-bound capabilities (e.g. `ABENNEWS-758-RESTFUL`)
   - `/cap-fiori-showcase` for an alternate CAP phrasing of the same concept
   - DDLX `annotate view` as a fallback when entity-level annotations are rejected on the DDLS projection (Run 7 evidence: 7.58 routes `@UI.facet`, `@UI.selectionFields`, and header `FOR_ACTION` through DDLX)

### Starter tag pointers (non-exhaustive — search for more)

Search-Term references that resolve to a concrete annotation pattern in the showcase. Each is the *entry point*; fetch the full content to get the canonical syntax for your release.

| Need | Starting search terms / docs |
|---|---|
| Colored status / criticality on a list row | `#LineItemHighlight`, `criticality: 'CriticalityCode'`, `@UI.Criticality` |
| Criticality with calculation logic (not a real column) | `/abap-docs-standard/ABENCDS_PROJ_VIEW_VIRTUAL_ELEMENT`; example `ABENCDS_PROJ_VIEW_VIRTEL_ABEXA`. Also `@ObjectModel.virtualElementCalculatedBy: 'ABAP:<class>'` |
| Header data point / status indicator | `#HeaderFacet`, `#KeyValue`, `#DataPointProgress`, `@UI.dataPoint` |
| Object Page table for a child entity | `#OPTable`, `@UI.lineItem` on the child projection, `@UI.facet` with `type: #LINEITEM_REFERENCE`, `targetElement: '_AssocName'` |
| Drill from one OP to another OP (e.g. Project → Task → TimeEntry) | `#OPTable` (entry); `cap-fiori-showcase/app/listreport-objectpage/layouts_ChildEntities1.cds` etc. (sub-page layout); SAPUI5 *"Configuring Internal Navigation"* (sap-help) for the manifest routing shape |
| In-page navigation between facets | `#InboundNav`, `targetSections`, `navigation` property on `@UI.facet` |
| Action button — OP header | `#OPHeaderAction`, `type: #FOR_ACTION`, `DataFieldForAction` |
| Action button — LR table toolbar | `#ActionInLineItem`, `#WithAction` |
| Sorting + filter bar | `#SelectionFields`, `#PresentationVariant`, `@UI.selectionFields` |
| Search across fields | `#Search`, `@Search.searchable`, `@Search.defaultSearchElement` |
| Semantic key (highlighted/bold in tables) | `#SemanticKey`, `@ObjectModel.semanticKey` |
| Value help / fixed value list | `#ValueHelp`, `@Consumption.valueHelpDefault`, `@ObjectModel.text.element` |
| Dynamic CRUD (hide/show edit/delete by row state) | `#DynamicCRUD`, `@UI.updateHidden`, `@UI.deleteHidden`, BDEF `features: instance` |
| Draft + UI specifics | `#DraftHandling`, `@UI.lineItem` editable qualifier, action keyword `( precheck )` |
| Side effects / re-read on action | `#SideEffects`, `@Common.SideEffects` |
| BDEF action signature variants (precheck, factory, copy) | `/abap-docs-standard/ABENBDL_ACTION`, `#OPCopyAction`, `action ( precheck ) <name>` |
| 7.58 capability boundaries | `/abap-docs-standard/ABENNEWS-758-RESTFUL` + `ABENNEWS-758-CDS_BDL` |

### Anti-patterns

- **Don't take this skill's example code as gospel.** It's a teaching example written at a
  point in time. The showcase is the ground truth.
- **Don't paste a showcase example from a different release without verifying.** A
  `#DataFieldForAction` example branching off the BTP-Cloud branch may use cloud-only
  keywords; the on-prem `main` branch is the safe baseline for 7.58.
- **Don't hand-code FE routing/manifest entries without consulting the SAPUI5 internal
  navigation docs.** The shape varies by template + release and is easier to derive from
  `sap-help` than to reverse-engineer from a runtime failure.
- **Don't accept the first failed-activation symptom as the final answer.** Run 7 hit four
  different blockers in four different layers for one polish item; in every case a follow-up
  search via `mcp__sap-docs__search` would have surfaced an alternative *before* the
  rollback. The cost of one extra search is 5 seconds; the cost of an unnecessary rollback
  is 10+ minutes.

## Why this is its own skill

Fiori Elements isn't a code transformation — it's a **deletion + relocation**. Most of the
freestyle app is replaced by FE templates that interpret CDS annotations. The skill's three
real jobs:

1. **Derive** the right `@UI.*` annotations from the legacy app's user-visible features so the
   FE renders an equivalent UX.
2. **Apply** those annotations to the RAP CDS projection (the service the SRVB exposes).
3. **Generate** the FE list-report + object-page project via the **SAP Fiori MCP server**
   (`@sap-ux/fiori-mcp-server`), and wire **extensions** for anything FE templates can't express.

## Smart defaults (apply silently — do NOT ask before research)

| Setting | Default | Rationale |
|---|---|---|
| Legacy app | `<source_app>/` | The freestyle JS app — read-only; mined in Phase 1 for the feature inventory. Authoritative source of every user-visible behavior the FE app must reproduce. |
| Target FE app | `<fe_app>/` — default name `modern-fe-app/` if user gives no path (mirrors the workspace pattern `legacy-ui5-app/` → `modern-ui5-app/` → `modern-fe-app/`) | Fiori MCP generates here; folder must exist and be empty (or empty subfolders) |
| FE floorplan | List Report + Object Page (LROP V4) when the BO has a clear root + child facets | Maps cleanly to the typical RAP composition root + children. OVP only if the user explicitly asks. |
| Template | `lropv4` | Default unless the user asks otherwise |
| Generator | **SAP Fiori MCP server (`@sap-ux/fiori-mcp-server`)** via the `list_functionalities` → `get_functionality_details` → `execute_functionality` sequence | First-party SAP tool; understands FE V4 patterns natively |
| App namespace | `<source_namespace>.fe` | Keeps the legacy and FE apps distinguishable (and the modern TS app too, if `modernize-ui5-app` was also run as the alternate path) |
| UI5 version | Latest 1.x release (e.g. `1.147.2`) unless user specifies | LTS-track; latest FE V4 features available |
| Language | TypeScript | Modern default; extension files use type-safe APIs |
| OData V4 service URL | **Prefer the SRVD-direct path** on the system's HTTPS port: `https://<host>:<https-port>/sap/opu/odata4/sap/<srvb>/srvd/sap/<srvb>/0001` (e.g. `https://a4h.marianzeis.de:50001/sap/opu/odata4/sap/zui_dm_projects_o4/srvd/sap/zui_dm_projects_o4/0001`). Falls back to the Gateway hub path `http://<host>:<http-port>/sap/opu/odata4/sap/<srvb>/srvd_a2x/sap/<service>/0001` only when SRVD-direct isn't reachable. | Run 5 + Run 6 verified the SRVD-direct path works without `/n/IWFND/MAINT_SERVICE` registration on 7.5x systems. The hub path requires the routing-group manual step. |
| Main entity | Root entity alias exposed by the SRVB (e.g. the alias on `define root view entity ... alias <X>`) | The LR+OP floorplan is rooted at a single entity |
| Annotations location | **In CDS via `SAPWrite update DDLS`** — not in a local annotation file inside the FE app | The annotations belong to the service; FE app reads them through `$metadata`. Local annotation files are an antipattern for RAP-bound apps. |
| Extension language | TypeScript (controller extensions) | Match the rest of the chain |
| Validation | `mcp__SAPUI5_MCP_Server__run_ui5_linter` + `run_manifest_validation` + `tsc --noEmit` all green | The hard acceptance criteria |
| Acceptance | FE app runs end-to-end against the V4 service. Browser smoke covers every feature inventoried in Phase 1 — reproduced through annotations or extension hooks. | Concrete deliverable |

## Input

The user provides:

- **Path** to the legacy app for feature mining (default: `<source_app>/`). Read-only.
- **Path / name** for the FE app (default: `<fe_app>/`).
- **OData V4 service URL** — derived from `migrate-segw-to-rap`'s SRVB output. Required.
- **Root entity alias** (e.g. `Project`). Default: infer from the SRVB's `$metadata`.
- **App namespace** (default: `<source_namespace>.fe`).
- **Transport** for the CDS annotation writes (default: same transport used by
  `migrate-segw-to-rap`, or auto-create via `SAPTransport(action="create")`).
- **UI5 version** (default: latest 1.x).

If only the legacy app path and V4 URL are provided, apply smart defaults and surface the plan
in Phase 3 for user `ok` before any writes.

---

## Phase 0 — Preflight

### 0a. RAP V4 service is live and bindable

```text
SAPManage(action="probe")
```

Assert `rap.available == true`.

```text
SAPRead(type="SRVB", name="<V4_SRVB>")
```

Assert the SRVB exists and is active. If not, stop with *"V4 service binding `<V4_SRVB>` is
missing or inactive — run `migrate-segw-to-rap.md` first."*

```text
Bash: curl -s -o /tmp/svc_metadata.xml -w "%{http_code}\n" "<V4_service_URL>/\$metadata?sap-client=<client>"
```

Assert 200 OK. **Try the SRVD-direct path first** (HTTPS port + `/srvd/sap/<srvb>/0001/`).

If 403 with `IWBEP/CM_V4_COS/136`, the URL is using the Gateway hub path and the routing
group isn't registered. **Switch to the SRVD-direct URL pattern before stopping** — the
direct path bypasses the hub registration. Only fall back to *"register service group via
`/n/IWFND/MAINT_SERVICE`"* if BOTH URL patterns fail.

If 503 with `IWBEP/CM_V4_RUNTIME/000` ("service alias cache outdated"), retry once — this is
a transient Gateway cache issue. If it persists across retries, treat as 403 and switch URL
patterns.

### 0b. CDS projection is readable + writable

```text
SAPRead(type="DDLS", name="<root_projection>")
```

Assert active version returns. If not, stop with *"Root projection is missing — re-run
`migrate-segw-to-rap.md` Phase 6 Step 3."*

### 0c. Legacy app exists and is readable

```text
Bash: ls <source_app>/webapp/controller/ <source_app>/webapp/view/
```

Assert both folders are populated. Stop with explicit reason if either is missing — the
legacy app is the single source of truth for both the annotation plan (Phase 1+2) and the
extension list (Phase 2b).

### 0d. SAP Fiori MCP server reachable

```text
mcp__fiori-mcp__list_functionalities
```

If this call fails, the Fiori MCP server isn't configured in this chat. Surface the
configuration block and stop:

```jsonc
// .cursor/mcp.json (or equivalent)
{
  "mcpServers": {
    "fiori-mcp": {
      "type": "stdio",
      "timeout": 600,
      "command": "npx",
      "args": ["--yes", "@sap-ux/fiori-mcp-server@latest", "fiori-mcp"]
    }
  }
}
```

(Source: [@sap-ux/fiori-mcp-server README on
github.com/SAP/open-ux-tools](https://github.com/SAP/open-ux-tools/tree/main/packages/fiori-mcp-server).)

Optional companion: `mcp__SAPUI5_MCP_Server__*` for post-generation lint + manifest validation.

### 0e. Connection Manager (optional)

For scenarios that use a saved BTP / on-prem system connection rather than a direct service
URL, the Fiori MCP server depends on the **Connection Manager for SAP Systems** VS Code
extension. For the direct-URL flow this skill uses, the extension is **not** required — the
service URL is passed straight through to `execute_functionality`.

---

## Phase 1 — Inventory the legacy app's user-visible features

This is the heart of the annotation plan. The FE app must reproduce every user-visible
behavior of the legacy app. Read **every** relevant legacy file and produce a feature
inventory.

### 1a. Read the legacy manifest

```text
Read: <source_app>/webapp/manifest.json
```

Pull: `sap.ui5.routing.routes` (what URLs the user can reach), `sap.ui5.rootView`,
`sap.app.dataSources`, `i18n`. The routes tell you the navigation tree; the data sources tell
you what service shape the legacy app expects.

### 1b. Read every legacy view

```text
Bash: ls <source_app>/webapp/view/
Read: <source_app>/webapp/view/<each>.view.xml
```

For each view, inventory:

- **Page title** (from `<Page title=...>`).
- **Header content** — buttons, with their conditions (e.g. `enabled="{= ${Status} === 'D'}"`).
- **List/table columns** — every `<Column>` with its bound field, sort direction, alignment.
- **Search fields** — `<SearchField>` with `liveChange` + `placeholder`.
- **Sort defaults** — `sorter` clause on list bindings (path + ascending/descending).
- **Item structure** — `ObjectListItem`/`ColumnListItem`/`ObjectHeader` with their attributes,
  status fields, formatter calls.
- **Tab structure** — `IconTabBar` + `IconTabFilter` entries (which entities are tabbed,
  whether each tab has a counter).
- **Navigation handlers** — `press`/`selectionChange` callbacks (rows clickable → navigation).
- **Counters, footers** — informational widgets (item counts, totals).

### 1c. Read every legacy controller

```text
Bash: ls <source_app>/webapp/controller/
Read: <source_app>/webapp/controller/<each>.controller.js
```

For each controller, inventory:

- **Action handlers** — what does each `on<Action>` method do? (Call OData function import?
  Filter? Re-bind? Show MessageBox?)
- **Filtering / search logic** — what fields, what `FilterOperator`, what bind path?
- **Function-import calls** — `oModel.callFunction(...)` calls and their parameter shapes.
- **Custom validation** — pre-call checks before function imports / submits.
- **Custom navigation** — anything that bypasses the manifest router.
- **Custom client-side state** — JSONModels created for UI-only state (e.g. tab counters fed
  from non-OData sources).

### 1d. Read every legacy formatter

```text
Read: <source_app>/webapp/model/formatter.js
```

For each formatter function, inventory:

- **What it formats** — status code → text, status code → semantic state (Success/Warning/Error),
  date → short format, hours → decimal, etc.
- **The code→text mapping** — capture the exact values (e.g. `D → "Draft"`, `A → "Active"`,
  `C → "Closed"`).

These map directly to CDS annotation patterns — `@Common.Text` + `@UI.TextArrangement`,
`@UI.Criticality`, value-help collections, etc.

### 1e. Build the feature inventory

Print the structured inventory to the user:

```text
Feature inventory — <source_app>:

ROOT ENTITY (Project):
  Display name (header):    Title
  Description (sub):        ProjectId
  Status (criticality):     Status field — values D=Draft (Warning) / A=Active (Success) / C=Closed (Neutral)
  Sort default:             StartDate descending
  Search (master):          ProjectId, Title (Contains, OR-joined)
  Audit fields:             Erdat (Created), Ernam (Created by), Aedat (Changed), Aenam (Changed by)
  Header button:            Approve — enabled when Status == 'D', calls /ApproveProject(ProjectId)
  Count footer:             "<n> projects" displayed below the list

CHILD ENTITY (Task):
  Tab in detail:            "Tasks"
  Counter:                  from binding length
  Columns:                  TaskId, Title+Description, Status, Priority, DueDate, AssignedTo, EstimatedHours
  Status criticality:       Status: D/IP/D/C (Draft/InProgress/Done/Cancelled) — see formatter
  Priority criticality:     L/M/H (Low/Medium/High) — see formatter
  Click behavior:           Selecting a task populates the TimeEntries tab

CHILD-OF-CHILD ENTITY (TimeEntry):
  Tab in detail:            "Time Entries"
  Counter:                  from selected task's TimeEntries length
  Columns:                  EntryId, TaskId, WorkDate, WorkHours, Description, Username

FORMATTERS:
  statusText:       D → "Draft", A → "Active", C → "Closed"
  statusState:      D → Warning, A → Success, C → Neutral
  taskStatusText:   D → "Draft", IP → "In Progress", DN → "Done", CN → "Cancelled"
  taskStatusState:  same scheme
  priorityText:     L → "Low", M → "Medium", H → "High"
  priorityState:    L → Neutral, M → Warning, H → Error
  dateShort:        custom YYYY-MM-DD
  hoursDecimal:     two decimal places

FUNCTION IMPORT (V2):
  ApproveProject(ProjectId) → translates to RAP action approve_project (already in BDEF)
```

This inventory is the contract for Phase 2.

---

## Phase 2 — Map features to annotations + extensions

Classify each inventory item as one of:

- **STANDARD** — FE templates render this natively (pagination, row click navigation,
  search-by-key, basic edit, basic create, basic delete, draft support).
- **ANNOTATION** — needs a specific `@UI.*` / `@Common.*` / `@ObjectModel.*` annotation on
  the CDS projection. Most legacy features land here.
- **EXTENSION** — genuinely custom behavior FE templates can't express; needs a controller
  extension on the FE app side.

### 2a. Annotation map — starting points (verify each pattern via sap-docs)

The table below is a **quick reference**, not a definitive catalog. Apply the search
recipe from the "Using `mcp__sap-docs__search`" section above to confirm the current
canonical syntax for your release, especially for anything tagged ⚠ below.

| Legacy feature | Starting annotation + showcase tag |
|---|---|
| Header title = `Title` | `@UI.HeaderInfo.title.value: 'Title'` — `#HeaderInfo` |
| Header description = `ProjectId` | `@UI.HeaderInfo.description.value: 'ProjectId'` — `#HeaderInfo` |
| Header type label | `@UI.HeaderInfo.typeName: 'Project'`, `typeNamePlural: 'Projects'` — `#HeaderInfo` |
| List columns (master) | `@UI.lineItem: [{ position: N, importance: #HIGH }]` per field — `#LineItemHighlight` for the variant with criticality |
| ⚠ Status code → text + color | **Multiple valid patterns; choose by release/draft constraints.** Search `#LineItemHighlight` for the criticality-via-real-column path. For a calculated criticality without a persistent column, see `/abap-docs-standard/ABENCDS_PROJ_VIEW_VIRTUAL_ELEMENT` + `@ObjectModel.virtualElementCalculatedBy: 'ABAP:<class>'`. On 7.58 + draft, both have known constraints (Run 7 §1 details). Search before adopting. |
| Sort default | `@UI.presentationVariant.sortOrder: [{ by: '<field>', direction: #DESC }]` — `#PresentationVariant` |
| Search across fields | `@Search.searchable: true` at view level + `@Search.defaultSearchElement: true` per field — `#Search` |
| Filter bar | `@UI.selectionFields: [...]` — `#SelectionFields` |
| Audit fields display | `@UI.fieldGroup` + `@UI.facet` of `#FIELDGROUP_REFERENCE` — `#HeaderFieldGroup` |
| Approve button (header) | `@UI.identification: [{ type: #FOR_ACTION, dataAction: 'approve_project', label: 'Approve', criticality: 3, criticalityRepresentation: #WITH_ICON }]` — `#OPHeaderAction`. **The action does NOT auto-render from the BDEF.** Projection BDEF must already expose `use action approve_project;`. |
| LR toolbar action | Same shape on `@UI.lineItem` — `#ActionInLineItem`, `#WithAction` |
| OP table for child entity | `@UI.facet: [{ type: #LINEITEM_REFERENCE, targetElement: '_AssocName', label: '...' }]` on the parent + `@UI.lineItem` on the child projection — `#OPTable` |
| ⚠ Multi-level navigation (Project → Task → TimeEntry) | Two valid shapes — **research before picking**. (a) **Nested LineItem facet** referencing `_Tasks._TimeEntries` from the root projection (release-dependent FE support). (b) **Separate Task OP route** — annotate `ZC_DM_TASK` with `@UI.headerInfo` + its own `_TimeEntries` facet, then add a Task routing target to the FE app's manifest. Search `#OPTable`, `#Subpages`, `#InboundNav` (cap-fiori-showcase); plus SAPUI5 *"Configuring Internal Navigation"* (sap-help) for manifest routing shape. |
| Semantic key bolding | `@ObjectModel.semanticKey: ['ProjectId']` — `#SemanticKey` |
| Dynamic CRUD (hide buttons by row state) | `@UI.updateHidden` + `@UI.deleteHidden` bound to a field — `#DynamicCRUD` |

> **DDLS-vs-DDLX scope on 7.58.** Several entity-level annotations
> (`@UI.facet`, `@UI.selectionFields`, header `FOR_ACTION` via `@UI.identification`) were
> rejected on the projection DDLS in Run 7 but succeeded when moved to a DDLX
> *metadata extension* (`annotate view <ZC_*> with @UI.facet: [...]` etc.). If a DDLS
> entity-level annotation errors with "wrong scope" / "unknown", route it through DDLX.
> Confirm with the relevant `#Tag` in `/abap-fiori-showcase` — the showcase patterns are
> already split between DDLS and DDLX where the on-prem release requires it.

### 2b. Extension map (typical)

| Legacy feature | Extension hook |
|---|---|
| Status === 'D' guard on Approve | FE's BDEF action button is enabled per the BDEF's `precheck`. If the legacy app's enable rule differs from the BDEF, **add the precheck on the BDEF** (Step 2c) rather than overriding in JS. |
| Custom validation before Approve (e.g. require Description) | `editFlow.onBeforeAction` controller extension on the ObjectPage |
| Custom client-side count footer phrasing | Skipped — FE list-report shows native count |
| Custom navigation patterns | Almost never needed; FE's row → OP nav is default |
| MessageBox on approve failure | Native FE error toast already handles this |

### 2c. BDEF-side preparation (run AFTER 2b, BEFORE Phase 4)

If the inventory surfaced a guard like "Approve button only when Status === 'D'", that's a
**precondition**, not a UI concern. Encode it on the BDEF side via the action's `precheck`
clause, then re-activate. (This step belongs in `migrate-segw-to-rap.md` Step 5 but call it
out explicitly here when the legacy guard wasn't already lifted.) Example:

```abap
action approve_project precheck result [1] $self;
```

with a `precheck` method in the behavior pool that fills `%cid` / `%key` and asserts
`Status == 'D'`. Skill defers the actual `precheck` body to `edit_method` if needed.

---

## Phase 3 — Plan + user approval

Print the plan in this exact format and STOP for `ok` / `edit` / question:

```text
Plan — generate FE app at <fe_app>:

Floorplan:           List Report + Object Page (LROP V4)
Namespace:           <source_namespace>.fe
UI5 version:         <user-provided, default latest 1.x>
V4 service:          <V4_service_URL>
Main entity:         <root_alias> (alias on the root projection)
Generator:           SAP Fiori MCP server (@sap-ux/fiori-mcp-server)

Backend annotation writes (via SAPWrite update DDLS on the projection):
  <root_projection>:
    @UI.HeaderInfo, @UI.SelectionFields, @UI.LineItem (root columns),
    @UI.Identification, @UI.PresentationVariant.SortOrder,
    @UI.FieldGroup.Audit, @UI.Facet (Identification + Tasks)
    @Common.Text + virtual <field>Criticality (Status)

  <child_projection_task>:
    @UI.LineItem (Task columns), @UI.HeaderInfo,
    @UI.Facet (Task identification + TimeEntries),
    @Common.Text + virtual fields for Status, Priority

  <child_projection_timeentry>:
    @UI.LineItem (TimeEntry columns), @UI.HeaderInfo

BDEF-side adjustments (via SAPWrite update BDEF / edit_method):
  <list precheck additions, if any>

FE app generation (via Fiori MCP):
  Step 1: list_functionalities      → enumerate supported FE app creations
  Step 2: get_functionality_details → "create FE V4 list-report+OP for external OData service"
  Step 3: execute_functionality     → pass service URL, root entity, namespace, target folder

Extension scaffold (post-generation):
  - <controller-extension list per Phase 2b>

Validation:
  - ui5-linter clean
  - manifest validation clean
  - tsc --noEmit clean
  - browser smoke against every Phase 1 inventory item

Type `ok` to proceed, `edit` to revise, or ask any question.
```

Wait for `ok` before mutating anything in the SAP system or generating the FE app.

---

## Phase 4 — Write `@UI.*` annotations to CDS projections (backend preparation)

This is the "prepare the annotations in the backend" step. Do **all** the CDS writes here
BEFORE invoking the Fiori MCP generator — the generator reads the annotated `$metadata`, so
the annotations must be active and the SRVB republished before the generator runs.

### 4a. Annotate the root projection

```text
SAPRead(type="DDLS", name="<root_projection>")
```

Splice in the planned annotations. Concrete shape (substitute your entities):

```cds
@Metadata.allowExtensions: true

@UI.headerInfo: {
  typeName:       'Project',
  typeNamePlural: 'Projects',
  title:          { value: 'Title' },
  description:    { value: 'ProjectId' }
}
@UI.selectionFields: [ 'ProjectId', 'Title', 'Status' ]
@UI.presentationVariant: [{
  qualifier: 'DefaultSort',
  sortOrder: [{ by: 'StartDate', direction: #DESC }]
}]
@UI.facet: [
  { id: 'GeneralInfo',  purpose: #STANDARD, type: #IDENTIFICATION_REFERENCE, label: 'General' },
  { id: 'AuditInfo',    purpose: #STANDARD, type: #FIELDGROUP_REFERENCE, label: 'Audit',
    targetQualifier: 'Audit' },
  { id: 'Tasks',        purpose: #STANDARD, type: #LINEITEM_REFERENCE, label: 'Tasks',
    targetElement: '_Tasks' }
]
// Header action — renders as an "Approve" button on the OP header (showcase #OPHeaderAction).
// criticality 3 = green (success), 0 = neutral, 1 = red.
@UI.identification: [
  { type: #FOR_ACTION,
    dataAction: 'approve_project',
    label: 'Approve',
    criticality: 3,
    criticalityRepresentation: #WITH_ICON }
]
@Search.searchable: true
define root view entity <root_projection>
  provider contract transactional_query
  as projection on <root_view>
{
  key   @UI.lineItem:       [{ position: 10, importance: #HIGH }]
        @UI.identification: [{ position: 10 }]
        @UI.selectionField: [{ position: 10 }]
        @Search.defaultSearchElement: true
        ProjectId,

        @UI.lineItem:       [{ position: 20, importance: #HIGH }]
        @UI.identification: [{ position: 20 }]
        @UI.selectionField: [{ position: 20 }]
        @Search.defaultSearchElement: true
        Title,

        @UI.lineItem:       [{ position: 30, criticality: 'StatusCriticality' }]
        @UI.identification: [{ position: 30 }]
        @UI.selectionField: [{ position: 30 }]
        Status,

        @UI.lineItem:       [{ position: 40 }]
        @UI.identification: [{ position: 40 }]
        StartDate,

        @UI.lineItem:       [{ position: 50 }]
        @UI.identification: [{ position: 50 }]
        EndDate,

        @UI.identification: [{ position: 60 }]
        Description,

        @UI.fieldGroup: [{ qualifier: 'Audit', position: 10, label: 'Created at' }]
        Erdat,
        @UI.fieldGroup: [{ qualifier: 'Audit', position: 20, label: 'Created by' }]
        Ernam,
        @UI.fieldGroup: [{ qualifier: 'Audit', position: 30, label: 'Changed at' }]
        Aedat,
        @UI.fieldGroup: [{ qualifier: 'Audit', position: 40, label: 'Changed by' }]
        Aenam,

        // virtual criticality field (computed from Status code)
        @ObjectModel.virtualElementCalculatedBy: 'ABAP:<root_class>'  // OR via case-when below
        virtual StatusCriticality : abap.int1,

        /* associations */
        _Tasks
}
```

For the virtual criticality, simplest cross-7.58-compatible option is a `case-when` directly
in the projection:

```cds
case Status
  when 'D' then 2    // Critical (Warning)
  when 'A' then 3    // Positive (Success)
  when 'C' then 0    // Neutral
  else          0
end as StatusCriticality,
```

Write the spliced source:

```text
SAPWrite(action="update", type="DDLS", name="<root_projection>",
         source="<spliced source>", transport="<transport>")
SAPActivate(type="DDLS", name="<root_projection>")
```

### 4b. Annotate every child projection

Repeat the pattern for each `<child_projection_X>`. Each child gets:

- `@UI.headerInfo` (title/description)
- `@UI.lineItem` for every column the legacy view showed
- `@UI.facet` if the child has further children (e.g. `_TimeEntries` under Task)
- Virtual criticality fields for any code-with-color field surfaced in the inventory

### 4c. Publish the SRVB

The SRVB needs republishing for the new annotations to surface in `$metadata`:

```text
SAPWrite(action="publish_srvb", name="<V4_SRVB>")
```

### 4d. Verify annotations land in `$metadata`

```text
Bash: curl -s "<base>/<V4_service_URL>/$metadata" | grep -oE 'UI\.(LineItem|HeaderInfo|Facets|SelectionFields|PresentationVariant|FieldGroup|Identification)' | sort -u
```

Expected output: every annotation kind you wrote shows up in the listing. If empty, the SRVB
didn't pick up the changes — re-run `publish_srvb`. If only some show, the projection wasn't
re-activated cleanly — `SAPDiagnose(action="object_state", type="DDLS", name="<root_projection>")`
to inspect.

---

## Phase 5 — Generate the FE app via the SAP Fiori MCP server

This is the "use Fiori MCP to create the app" step. The Fiori MCP server exposes a
three-tool dance — `list_functionalities` → `get_functionality_details` → `execute_functionality`.
Each step narrows scope. Do not skip steps even if you think you know the params.

References:
- [SAP/open-ux-tools — fiori-mcp-server README](https://github.com/SAP/open-ux-tools/tree/main/packages/fiori-mcp-server)
- [npm: @sap-ux/fiori-mcp-server](https://www.npmjs.com/package/@sap-ux/fiori-mcp-server)
- [SAP Community: First Release of the SAP Fiori MCP Server](https://community.sap.com/t5/technology-blog-posts-by-sap/sap-fiori-tools-update-first-release-of-the-sap-fiori-mcp-server-for/ba-p/14204694)

> The Fiori MCP server is **experimental** at the time of this skill's writing. Tool
> signatures may change. If `list_functionalities` returns a different shape than expected,
> follow the live response — the README above is the source of truth.

### 5a. Discover what the server can do

```text
mcp__fiori-mcp__list_functionalities
```

Inspect the response for a functionality that matches "create a new Fiori elements application
for an external OData V4 service". The exact name in the response depends on the server version
— look for keywords like `create`, `fiori-elements`, `list-report`, `lropv4`, `external-service`.

If the server doesn't have a matching functionality:

- It might require a CAP project. In that case, generate via the legacy fallback
  (`@sap/generator-fiori` CLI — see 5d) and document the gap as a Run capture for next iteration.
- Or the user's Fiori MCP version is older than expected. Re-check by running:
  ```text
  Bash: npx --yes @sap-ux/fiori-mcp-server@latest --version
  ```
  and instructing the user to update the MCP config to pin the latest.

### 5b. Get parameter requirements for the chosen functionality

```text
mcp__fiori-mcp__get_functionality_details(name="<the-functionality-name-from-5a>")
```

The response lists required + optional parameters. For the LROP-for-external-service flow,
typical required parameters include:

- `serviceUrl` — `<V4_service_URL>` (the `$metadata`-backed endpoint from Phase 0a)
- `mainEntity` or `entitySet` — the root entity alias from the SRVB (e.g. `Project`)
- `targetPath` — `<fe_app>/` (the folder to generate into)
- `namespace` — `<source_namespace>.fe`
- `appId` / `appName` — the application identifier
- `ui5Version` — user-provided (default: latest 1.x)
- `language` — `typescript`

Map the user-supplied inputs onto the required parameters. If a required parameter has no
mapping, **stop and ask the user**. Do not invent values.

### 5c. Execute the generator

```text
mcp__fiori-mcp__execute_functionality(
  name="<the-functionality-name-from-5a>",
  parameters={
    serviceUrl: "<V4_service_URL>",
    mainEntity: "<root_alias>",
    targetPath: "<fe_app>",
    namespace:  "<source_namespace>.fe",
    appName:    "<short-app-id>",
    ui5Version: "<ui5-version>",
    language:   "typescript"
  }
)
```

Wait for the generator to finish. Verify the structure:

```text
Bash: ls <fe_app>/webapp/ && cat <fe_app>/webapp/manifest.json | head -40
```

Expected:

- `webapp/Component.ts`
- `webapp/manifest.json` with `sap.ui.generic.app` / `sap.fe.templates` config
- `webapp/i18n/i18n.properties`
- `webapp/ext/` (extension folder — may be empty)
- `package.json` / `ui5.yaml` / `tsconfig.json`

### 5d. Fallback if the MCP server can't do it

If `list_functionalities` doesn't expose an external-service path, fall back to the
`@sap/generator-fiori` CLI:

```text
Bash: cd <workspace> && npx --yes @sap/generator-fiori --no-deploy
```

Walk through the prompts (or pass `--skip-install` with explicit args). This is the same
generator the MCP server wraps — slower because it's interactive, but always works.

### 5e. Known Fiori MCP rough edges (track upstream)

The Fiori MCP server is **experimental**. Two known issues to recognize and work around:

- **`fetch-service-metadata` returns an MCP framework error instead of a structured response**
  when the `@sap-ux/store` has no entry for the target system. Workaround: `curl $metadata`
  yourself to a local file (e.g. `<fe_app>/metadata.xml`), then point `execute_functionality`
  at the file. Tracked upstream:
  [SAP/open-ux-tools#4652](https://github.com/SAP/open-ux-tools/issues/4652) — when this
  closes, the curl workaround can be dropped.
- **`fetch-service-metadata` requires Connection Manager configuration** for stored
  system entries; direct URL passthrough to `execute_functionality` bypasses this. Skill's
  Phase 0e already documents the direct-URL path.

Capture any **new** rough edges in `RUN-NOTES.md` so the next iteration can either work
around them or file follow-up upstream issues.

---

## Phase 6 — Configure extensions for the EXTENSION list from Phase 2b

For each row in the Phase 2b extension map, scaffold a TS controller extension. FE V4
extensions come in two flavors:

- **Controller extensions** — extend the LR or OP controller via the `editFlow` /
  `routing` / `appComponent` API.
- **Extension points** — slot custom XML/Fragment into a specific spot (header, footer,
  before/after a section).

For the talk demo's typical extension (a precheck on approve that the BDEF doesn't already
encode):

```text
Write: <fe_app>/webapp/ext/ObjectPageExt.ts
```

```typescript
import ControllerExtension from "sap/ui/core/mvc/ControllerExtension";
import MessageBox from "sap/m/MessageBox";

/**
 * @namespace <source_namespace>.fe.ext
 */
export default class ObjectPageExt extends ControllerExtension {
  public static overrides = {
    editFlow: {
      onBeforeAction: async function (
        this: ObjectPageExt,
        mParameters: { actionName: string; context: any }
      ) {
        if (!mParameters.actionName.endsWith(".approve_project")) return;
        const description = mParameters.context.getProperty("Description");
        if (!description) {
          MessageBox.error("Cannot approve a project without a description.");
          throw new Error("Approval blocked by extension");
        }
      }
    }
  };
}
```

Register the extension in `<fe_app>/webapp/manifest.json`:

```json
"sap.ui5": {
  "extends": {
    "extensions": {
      "sap.ui.controllerExtensions": {
        "sap.fe.templates.ObjectPage.ObjectPageController": {
          "controllerName": "<source_namespace>.fe.ext.ObjectPageExt"
        }
      }
    }
  

…(truncated)
