---
title: Application Cockpit Cards
aliases: [cockpit, card, CardContainer, dashboard, home screen]
sources:
    [
        sources/sessions/2026-02-18-application-cockpit-analysis.md,
        sources/sessions/2026-02-18-cardmytasks-implementation.md,
        sources/sessions/mfg-app/2026-04-22-build-error-learnings.md,
    ]
last_updated: 2026-04-28
status: draft
---

# Application Cockpit Cards

Adding a card to the Application Cockpit is one of the most common feature tasks. It requires coordinating changes across multiple files and layers.

## Overview

The cockpit uses a **lazy-loading pattern**: cards are created empty in EntryActions and only load data when they become visible on screen. A central controller (`BoSalesCockpitHelper`) manages card visibility and collapse state.

## Required Files to Create

| Layer | File                                                     | Purpose                 |
| ----- | -------------------------------------------------------- | ----------------------- |
| DS    | `DsBo{Card}_sf.datasource.xml`                           | Single item datasource  |
| DS    | `DsLo{Card}_sf.datasource.xml`                           | List datasource         |
| DS    | `DsLo{Card}ContextMenu_sf.datasource.xml`                | Context menu DS (empty) |
| BO    | `Bo{Card}/Bo{Card}.businessobject.xml`                   | Single item BO          |
| LO    | `Lo{Card}/Lo{Card}.listobject.xml`                       | Card list object        |
| LI    | `Lo{Card}/Li{Card}.listitem.xml`                         | Card list item          |
| LO    | `Lo{Card}ContextMenu/Lo{Card}ContextMenu.listobject.xml` | Context menu LO         |
| LI    | `Lo{Card}ContextMenu/Li{Card}ContextMenu.listitem.xml`   | Context menu LI         |
| BL    | `Lo{Card}/Mv2/Lo{Card}.GetTasksForCard.bl.js`            | Card data loading       |
| BL    | `Lo{Card}/Mv2/Lo{Card}.GetInfoForCard.bl.js`             | "X / Y" info text       |
| BL    | `Lo{Card}/Mv2/LoadAsync/*.bl.js`                         | Lifecycle hooks         |
| BL    | `Lo{Card}ContextMenu/Mv2/LoadAsync/*.bl.js`              | Context menu builder    |

## Required Files to Modify

| File                                           | Changes                                         |
| ---------------------------------------------- | ----------------------------------------------- |
| `Application_CockpitProcess.processflow.xml`   | Add Declarations, EntryActions, Events, Actions |
| `Application_CockpitUI.userinterface.xml`      | Add CardContainer with bindings                 |
| `BoSalesCockpitHelper.businessobject.xml`      | Add collapse/empty state properties             |
| `BoSalesCockpitHelper.IsCardVisible.bl.js`     | Add case to switch                              |
| `BoSalesCockpitHelper.IsCardCollapsible.bl.js` | Add case to switch                              |

## Card Controller (BoSalesCockpitHelper)

**This is the most commonly missed step.** Without it, your card will be invisible.

**1. Add properties:**

```xml
<SimpleProperty name="collapseState_Card{Name}" type="DomBool" />
<SimpleProperty name="card{Name}_emptyMainMessage" type="DomText" />
<SimpleProperty name="card{Name}_emptySubMessage" type="DomText" />
```

**2. Add case to IsCardVisible.bl.js:**

```javascript
case "Card{Name}":
    visible = true;
    break;
```

**3. Add case to IsCardCollapsible.bl.js:**

```javascript
case "Card{Name}":
    collapsible = true;
    break;
```

## Process Declarations

```xml
<Declaration name="Card{Name}_InformationText" type="String" />
<Declaration name="Card{Name}_List" type="Lo{Card}" />
<Declaration name="Card{Name}_ContextMenuList" type="Lo{Card}ContextMenu" />
<Declaration name="Card{Name}_DataLoaded" type="DomBool" />
<Declaration name="Card{Name}_Detail" type="Bo{Card}" />
```

## EntryActions (CREATE empty list)

```xml
<Action name="Card{Name}_GetList" actionType="CREATE" type="Lo{Card}">
  <Return name="ProcessContext::Card{Name}_List" />
</Action>
```

## Standard Event Registrations

```xml
<Event name="Card{Name}_loadData" action="Card{Name}_SetDataReady" />
<Event name="Card{Name}_itemSelected" action="Card{Name}_ShowDetail" />
<Event name="Card{Name}_contextMenuOpening" action="Card{Name}_GetContextMenu" />
<Event name="Card{Name}_contextMenuItemSelected" action="Card{Name}_ContextMenu_Decision" />
```

## Standard Action Chain (Load Flow)

```xml
<!-- 1. Set data loaded flag -->
<Action actionType="LOGIC" name="Card{Name}_SetDataReady" call="Utils.identity">
  <Parameters>
    <Input name="value" value="1" type="Literal" />
  </Parameters>
  <Return name="ProcessContext::Card{Name}_DataLoaded" />
  <TransitionTo action="Card{Name}_LoadItems" />
</Action>

<!-- 2. Call LO method to load items -->
<Action actionType="LOGIC" name="Card{Name}_LoadItems"
        call="ProcessContext::Card{Name}_List.getTasksForCard">
  <Parameters>
    <Input name="currentDate" value="ProcessContext::CardDate" />
    <Input name="numberOfListItems"
           value="ProcessContext::CardController.numberOfListItems" />
  </Parameters>
  <TransitionTo action="Card{Name}_GetCardInformation" />
</Action>

<!-- 3. Get info text "X / Y" -->
<Action actionType="LOGIC" name="Card{Name}_GetCardInformation"
        call="ProcessContext::Card{Name}_List.getInfoForCard">
  <Return name="ProcessContext::Card{Name}_InformationText" />
</Action>
```

## UI CardContainer

**Build-critical rules:**

1. `isCollapsible` is NOT an XML attribute — it's a **Binding** (`target="IsCollapsible"`, with a `call` to the controller method). Using `isCollapsible="true"` as an attribute causes error `00000001`.
2. `<LoadContainerData>` is a **self-closing element inside `<Events>`**, not a wrapper element. The correct form is `<LoadContainerData event="Card{Name}_loadData" />`. Nesting `<Events><CardLoadEvent .../></Events>` inside `<LoadContainerData>` causes error `00000001`.
3. Every CardContainer **must** have both `IsReadyToLoad` binding and `LoadContainerData` event — missing either causes error `00000016` / `00000018`.
4. `<VisibilityRoles>` comes BEFORE `<Events>` in element order.
5. CockpitList `dataSource` uses capital-I `.Items[]` (e.g., `ProcessContext::Card{Name}_List.Items[]`).
6. `<Items name="Items" itemPattern="...">` may include a sibling `<Bindings>` block (used by most working cards to bind column targets to LI properties). The wiki rule "not nested `<Binding>` elements" refers specifically to `<Binding>` directly inside a `<Col>` — the sibling `<Bindings>` block under `<Items>` is required whenever `<Col>`/`<Row>` has `bindingId` attributes.
7. **A CardContainer with a `<CockpitList>` is a multi-subcomponent container. It renders exactly one child at a time, picked by name.** There must be a `DisplayedSubcomponentName` binding that resolves to either the `<CockpitList>`'s `name` (show the list) or the `<NoDataMessage>`'s `name` (show the empty state). If no `DisplayedSubcomponentName` is provided, neither child matches — the runtime renders "We couldn't render this component." See the incident log below for how this was discovered.
8. `<CockpitList name="X">` must match the `type` literal passed into `BoSalesCockpitHelper.getDisplayedSubcomponentName` in the process. Convention across all shipped cards: `<CockpitList name="Visits">`, `"Activities"`, `"QuickAccess"`, etc. — **no `List` suffix**.
9. `<NoDataMessage name="CardNoDataMessageUiPlugin">` — this is a registered UIPlugin name, not a free identifier. Inventing a name like `"Card{Name}NoDataMessage"` silently breaks the empty-state branch.
10. `ItemListLayout` should include all three device variants (`<Default>`, `<Tablet>`, `<Phone>`) — every shipped list card does. A `<Default>`-only layout has been observed to render correctly in desktop simulators but is not the established pattern.
11. `layoutType="itemIdentifierCockpit"` must be on a `<Row>`, never directly on a `<Col>`. Wrap it: `<Col flex="1"><Row layoutType="itemIdentifierCockpit" bindingId="..."/></Col>`.

```xml
<CardContainer name="Card{Name}">
  <Bindings>
    <Binding type="Visible" target="Visible"
             call="ProcessContext::CardController.isCardVisible">
      <Parameters>
        <Input name="cardName" type="Literal" value="Card{Name}" />
      </Parameters>
    </Binding>
    <Binding type="Text" target="IsCollapsible"
             call="ProcessContext::CardController.isCardCollapsible"
             bindingMode="ONE_WAY">
      <Parameters>
        <Input name="cardName" type="Literal" value="Card{Name}" />
      </Parameters>
    </Binding>
    <Resource target="Title" type="Label" id="CardTitle" defaultLabel="{Card Title}" />
    <Binding target="Information" type="Text"
             binding="ProcessContext::Card{Name}_InformationText"
             bindingMode="ONE_WAY" />
    <Binding target="IsReadyToLoad" type="Text"
             binding="ProcessContext::Card{Name}_DataLoaded"
             bindingMode="ONE_WAY" />
    <Binding target="CollapseState" type="Text"
             binding="ProcessContext::CardController.collapseState_Card{Name}"
             bindingMode="TWO_WAY" />
    <!-- REQUIRED for any CardContainer that has a <CockpitList> child. -->
    <!-- Without this, the container cannot pick between <CockpitList> and <NoDataMessage>. -->
    <Binding target="DisplayedSubcomponentName" type="Text"
             binding="ProcessContext::Card{Name}_DisplayedSubcomponentName"
             bindingMode="ONE_WAY" />
  </Bindings>
  <VisibilityRoles allRoles="true" />
  <Events>
    <LoadContainerData event="Card{Name}_loadData" />
  </Events>
  <NoDataMessage name="CardNoDataMessageUiPlugin">
    <Bindings>
      <Binding target="maintext" type="Text"
               binding="ProcessContext::CardController.card{Name}_emptyMainMessage"
               bindingMode="ONE_WAY" />
    </Bindings>
  </NoDataMessage>
  <!-- CockpitList name MUST match the `type` literal used in getDisplayedSubcomponentName. -->
  <!-- Convention: single noun, no "List" suffix. e.g., name="{Name}", not "{Name}List". -->
  <CockpitList name="{Name}"
               hasBorder="false"
               dataSource="ProcessContext::Card{Name}_List.Items[]">
    <Items name="Items" itemPattern="Card{Name}Items">
      <ItemListLayout>
        <Default>
          <Col flex="1">
            <Row layoutType="itemIdentifierCockpit" bindingId="name" />
          </Col>
          <Col width="5em" layoutType="itemSecondaryCockpit" bindingId="status" />
        </Default>
        <Tablet>
          <Default>
            <Col flex="1">
              <Row layoutType="itemIdentifierCockpit" bindingId="name" />
            </Col>
            <Col width="5em" layoutType="itemSecondaryCockpit" bindingId="status" />
          </Default>
        </Tablet>
        <Phone>
          <Default>
            <Col flex="1">
              <Row layoutType="itemIdentifierCockpit" bindingId="name" />
            </Col>
            <Col width="5em" layoutType="itemSecondaryCockpit" bindingId="status" />
          </Default>
        </Phone>
      </ItemListLayout>
      <Bindings>
        <Binding target="name" type="Text" binding=".name" bindingMode="ONE_WAY" />
        <Binding target="status" type="Text" binding=".status" bindingMode="ONE_WAY" />
      </Bindings>
    </Items>
  </CockpitList>
</CardContainer>
```

### Companion process wiring: the `AssignDisplayedSubcomponentName` step

The `DisplayedSubcomponentName` binding above reads from a ProcessContext slot that is populated by a dedicated action at the end of the load chain. The action calls `BoSalesCockpitHelper.getDisplayedSubcomponentName(loItems, type)`, which returns either the card's subcomponent name (when `loItems.getCount() > 0` — show the list) or `"CardNoDataMessageUiPlugin"` (when empty — show the empty state).

Append this action to the load chain after `Card{Name}_GetCardInformation`:

```xml
<Action actionType="LOGIC" name="Card{Name}_GetCardInformation"
        call="ProcessContext::Card{Name}_List.getInfoForCard">
  <Return name="ProcessContext::Card{Name}_InformationText" />
  <TransitionTo action="AssignDisplayedSubcomponentNameFor{Name}" />
</Action>
<Action name="AssignDisplayedSubcomponentNameFor{Name}" actionType="LOGIC"
        call="ProcessContext::CardController.getDisplayedSubcomponentName">
  <Parameters>
    <Input name="loItems" value="ProcessContext::Card{Name}_List" />
    <Input name="type" value="{Name}" type="Literal" />
  </Parameters>
  <Return name="ProcessContext::Card{Name}_DisplayedSubcomponentName" />
</Action>
```

**The `type` literal value must match the `name` attribute on the `<CockpitList>` exactly.** If they drift, the container renders neither subcomponent and the user sees the "We couldn't render this component" fallback.

No `<Declaration>` is needed for `Card{Name}_DisplayedSubcomponentName` — the `<Return>` implicitly creates the ProcessContext slot on first execution. This matches every shipped card's pattern.

## Cockpit Lazy Load Flow

```
User opens Home Screen
    → Application_CockpitProcess EntryActions
    → CREATE empty LOs for each card (FAST initial load)
    → UI displays (cards empty, not yet visible)
    → User scrolls → card becomes visible
    → CardLoadEvent triggers → "Card{Name}_loadData"
    → SetDataReady → LoadItems → GetCardInformation
    → Card renders with data and "X / Y" header
```

## Build-Critical: `onAutoReload` Has a Workspace-Wide Cap of 3 Files

The workspace validator enforces that **no more than three files** in `src/` may register a card for `onAutoReload`. Exceeding the cap fails the build with validator code `00000029`:

```
<source-file>:<line> | More than three card container registered for onAutoReload. | 00000029
```

Today the three registrations are:

-   `src/Application/PR/Application_Cockpit/Application_CockpitUI.userinterface.xml` (`SyncCardContainer cardSync`)
-   `src/Application/PR/Application_UserCockpit/Application_UserCockpitUI.userinterface.xml` (sync card)
-   `src/Tour/PR/Tour_DriverCockpit/Tour_DriverCockpitUI.userinterface.xml` (sync card)

### When you're cloning a cockpit

If you clone an existing cockpit UI (for example, to create a parallel-app cockpit like `Application_MfgCockpit`), the clone inherits the source's `<CardEventSubcription><CardEvent name="onAutoReload" .../>` block. That pushes the workspace to **four** subscribers and fails the build.

**Fix:** remove the `CardEventSubcription` block from the cloned file's SyncCardContainer (the sync strip still renders — it just loses auto-refresh):

```xml
<!-- WRONG in a cloned cockpit — pushes workspace to 4 subscribers -->
<SyncCardContainer name="cardSync">
  ...
  <VisibilityRoles allRoles="true" />
  <CardEventSubcription>
    <CardEvent name="onAutoReload" intervalInSeconds="10" />
  </CardEventSubcription>
</SyncCardContainer>

<!-- CORRECT in a cloned cockpit — drop the subscription -->
<SyncCardContainer name="cardSync">
  ...
  <VisibilityRoles allRoles="true" />
</SyncCardContainer>
```

Before adding a new `onAutoReload` subscription anywhere, confirm the count:

```bash
grep -rln 'onAutoReload' src/
# Expected: at most 3 files. If you need a 4th, one of the existing three must give it up first.
```

## Troubleshooting Card Visibility

If your card doesn't appear:

1. Verify card name matches **exactly** (case-sensitive) across all files
2. Check `IsCardVisible.bl.js` has a case for your card name
3. Check `IsCardCollapsible.bl.js` has a case for your card name
4. Verify `collapseState_Card{Name}` property exists in `BoSalesCockpitHelper.businessobject.xml`
5. Check `VisibilityRoles` in UI matches the user's role

## Best Practices

| Do                                                 | Don't                                        |
| -------------------------------------------------- | -------------------------------------------- |
| Update CardController first                        | Build entire card before checking visibility |
| Use "Card" prefix consistently                     | Use inconsistent naming                      |
| Use `CREATE` (not `LOAD`) in cockpit EntryActions  | Load data eagerly in EntryActions            |
| Implement device-aware limits (3 phone / 5 tablet) | Show same count on all devices               |
| Test build after each phase                        | Build only at the end                        |

## Incident Log: "We couldn't render this component" (2026-05-05)

**Symptom.** Two new cockpit cards (`CardVisitsForToday`, `CardOrdersToday`) showed the correct title and info text (e.g., `1 / 1`) but the body rendered the empty-state illustration with the text `"We couldn't render this component."` Meanwhile every other card on the same screen rendered fine.

**What was true.**

-   Build passed. All XML was well-formed.
-   The DataSource SQL ran (confirmed in `appl/data/log/backend.log`) and returned rows.
-   The BL populated the LO via `Facade.getListAsync + me.addItems + return me`.
-   The ProcessContext debugger showed the LO with its items (`_filtered: Array(1), _all: Array(1)`).
-   The header info text computed correctly from `me.cardItemCount`.

**What was false (a long detour).** The first hypothesis was that the CockpitList's row template was broken. Multiple rounds of diffing working cards against the broken one surfaced several real but non-blocking differences:

| Observed difference                                                                | Verdict                                         |
| ---------------------------------------------------------------------------------- | ----------------------------------------------- |
| Wrong `<NoDataMessage name>` (free identifier vs `"CardNoDataMessageUiPlugin"`)    | Real bug — fixed early                          |
| Wrong `VisibilityRoles` value (`"all"` instead of `"true"`)                        | Real bug — fixed                                |
| `layoutType="itemIdentifierCockpit"` directly on `<Col>` instead of inside `<Row>` | Real convention issue — fixed                   |
| `<Col layoutType="Image" bindingId="icon">` with no matching LI property           | Real bug — fixed                                |
| `DomDateTime` vs `DomText` for the time column                                     | Red herring                                     |
| `generateLoadMethod="true"` vs `"false"`                                           | Red herring — all working patterns use `"true"` |
| `<Return>` on the `LoadItems` process action                                       | Red herring                                     |
| Whether the BL `.then` returns `me`                                                | Red herring for rendering (still good practice) |

None of these fixes moved the card body past the render error.

**Root cause.** The debugger dump of ProcessContext exposed the real defect. Every working list card had a property that our two cards lacked:

```
✓ cardActivities_DisplayedSubcomponentName: "Activities"
✓ cardQuickAccess_DisplayedSubcomponentName: "QuickAccess"
✓ cardCustomerTasks_DisplayedSubcomponentName: "CustomerTasks"
✓ cardNotifications_DisplayedSubcomponentName: "CardNoDataMessageUiPlugin"
✓ cardTasks_DisplayedSubcomponentName: "CardNoDataMessageUiPlugin"
✗ cardVisitsForToday_DisplayedSubcomponentName:  (MISSING)
✗ cardOrdersToday_DisplayedSubcomponentName:     (MISSING)
```

**The framework rule.** A `CardContainer` with a `<CockpitList>` is a multi-child container. It renders exactly one child subcomponent at a time — either the `<CockpitList>` or the `<NoDataMessage>` — by matching the `DisplayedSubcomponentName` binding to a child's `name` attribute:

| `DisplayedSubcomponentName` value | Child rendered                                                    |
| --------------------------------- | ----------------------------------------------------------------- |
| matches `<CockpitList name="X">`  | the list                                                          |
| `"CardNoDataMessageUiPlugin"`     | the empty state                                                   |
| `null` / undefined                | **neither — runtime renders "We couldn't render this component"** |

The value is produced by `BoSalesCockpitHelper.getDisplayedSubcomponentName(loItems, type)` which returns `type` when the LO has rows and `"CardNoDataMessageUiPlugin"` when it doesn't.

**What made it work.** Three coupled additions:

1. **UI Binding** on the CardContainer:
    ```xml
    <Binding target="DisplayedSubcomponentName" type="Text"
             binding="ProcessContext::Card{Name}_DisplayedSubcomponentName"
             bindingMode="ONE_WAY" />
    ```
2. **Process action** chained from `GetCardInformation`:
    ```xml
    <Action name="AssignDisplayedSubcomponentNameFor{Name}" actionType="LOGIC"
            call="ProcessContext::CardController.getDisplayedSubcomponentName">
      <Parameters>
        <Input name="loItems" value="ProcessContext::Card{Name}_List" />
        <Input name="type" value="{Name}" type="Literal" />
      </Parameters>
      <Return name="ProcessContext::Card{Name}_DisplayedSubcomponentName" />
    </Action>
    ```
3. **`<CockpitList name="X">` matching the `type` literal** — e.g., `name="VisitsForToday"` + `value="VisitsForToday"`. Previously the list was named `"VisitsForTodayList"` (a `List` suffix mismatch) which would have failed the subcomponent lookup even with the binding in place.

### Naming convention learned

Every cockpit-card subcomponent is resolved **by string match** at render time. Four string identifiers must line up:

| Identifier                                    | Example                                                                         | Where set                                            |
| --------------------------------------------- | ------------------------------------------------------------------------------- | ---------------------------------------------------- |
| CardContainer name                            | `CardVisitsForToday`                                                            | UI `<CardContainer name="...">`                      |
| IsCardVisible switch case                     | `"CardVisitsForToday"`                                                          | BL `switch (cardName) { case ... }`                  |
| IsCardCollapsible switch case                 | `"CardVisitsForToday"`                                                          | same                                                 |
| CollapseState / emptyMessage property         | `collapseState_CardVisitsForToday`, `cardVisitsForToday_emptyMainMessage`       | `BoSalesCockpitHelper` SimpleProperties              |
| CockpitList name                              | `VisitsForToday` (**no `Card` prefix, no `List` suffix**)                       | UI `<CockpitList name="...">`                        |
| `getDisplayedSubcomponentName` `type` literal | `"VisitsForToday"`                                                              | process action `<Input name="type" ... value="...">` |
| NoDataMessage name                            | `CardNoDataMessageUiPlugin` (registered UIPlugin, **always this exact string**) | UI `<NoDataMessage name="...">`                      |

Any drift between the CockpitList `name` and the `type` literal silently breaks the list render. Any drift between NoDataMessage `name` and `"CardNoDataMessageUiPlugin"` silently breaks the empty state. Either failure surfaces as the generic "We couldn't render this component."

### Why all our search heuristics missed it

-   `sf mdl build` passed — this is a runtime contract, not a build-time one.
-   The `backend.log` only records DB traffic; the render decision is a client-side branch over the ProcessContext state, which never logs.
-   The `DisplayedSubcomponentName` wiring is spread across three files (process Declaration optional, process Action, UI Binding) — none of the three is obviously part of a "minimal list card."
-   The existing wiki/skill template for `<CardContainer>` did not include the `DisplayedSubcomponentName` binding. Every shipped card has it; it was implicit tribal knowledge.

The fix is to make it explicit. See the updated template above, the updated `add-cockpit-card` skill, and failure mode **Fm9** in the skill's troubleshooting table.

## Cross-References

-   [[processes]] — Cockpit process orchestrates all card loading
-   [[user-interface]] — CardContainer and CockpitList controls
-   [[list-objects]] — Each card uses an LO for its item list
-   [[business-logic]] — getTasksForCard and getInfoForCard patterns
