npx skills add ...
npx skills add arc-mcp/arc-1 --skill generate-analytics-star-schema
Generate a CDS analytical model (star schema — cube + dimension + text views) on top of a RAP business object or a DDIC table. Use when asked to "create a star schema", "generate an analytical cube and dimensions", "make a RAP BO analytical", "build a CDS cube from a table", or "create an analytical model".
npx skills add arc-mcp/arc-1 --skill generate-analytics-star-schema
Generate a CDS analytical model — a star schema consisting of a cube view (@Analytics.dataCategory: #CUBE), one or more dimension views (#DIMENSION), and text views (#TEXT) — on top of a RAP business object or a plain DDIC table.
This skill replicates SAP Joule's "CDS Analytical Model Generation" capability (basic + extended scope) by combining ARC-1 (SAP system access) with mcp-sap-docs (documentation & best practices). Basic scope = cube + reuse existing dimensions. Extended scope = also generate new dimensions, and start from a DDIC table (not just a RAP BO).
SAPManage(action="probe") (rap.available). If RAP/CDS isn't available, stop.SAPWrite(action="batch_create", activateAtEnd: true) for exactly that.| Setting | Default | Rationale |
|---|---|---|
| Object type | DDLS | Cube/dimension/text are all CDS data definitions |
| Package | $TMP | Fast prototyping; ask before a transportable package |
| Activation | batch_create + activateAtEnd: true | Cross-references resolve in one terminal pass |
| Authorization | #NOT_REQUIRED (cube/dimension) | Standard for analytical interface views |
| Naming | ZI_<X>_Cube, ZI_<X>_Dim, ZI_<X>_Txt | Clear star-schema roles (match the casing the templates use) |
The user provides either:
ZBP_R_TRAVEL or ZI_Travel), orZTRAVEL), orOptionally: which numeric fields are measures, which fields are dimensions, target package, and whether to generate new dimension views or reuse existing ones.
If the user names a behavior pool class (ZBP_*), read the class metadata to find the root entity it's bound to:
Look for the bound root entity reference. Then read that CDS root entity (next step). If the user already named the CDS root entity / interface view, skip to 1c.
Capture the field list with types. Identify candidate keys, dimension fields (foreign keys, characteristics), and numeric measure fields (amounts, quantities, counts).
The element list classifies fields. Decide for each:
@Aggregation.default: #SUMFor each dimension field, check whether a reusable dimension view already exists before generating a new one:
If a released #DIMENSION view exists (e.g. I_Country, I_CalendarDate), reuse it via association. Otherwise generate a new ZI_<X>_DIM (extended scope).
Ground the generation in current docs:
Cite the doc IDs in your summary. Key facts (7.58):
@Analytics: { dataCategory: #CUBE, internalName: #LOCAL } + @ObjectModel: { supportedCapabilities: [ #ANALYTICAL_PROVIDER ], modelingPattern: #ANALYTICAL_CUBE }@Aggregation.default: #SUM (or #MIN/#MAX/#AVG); a numeric field WITHOUT @Aggregation.default is treated as a dimension[1..1]; @ObjectModel.foreignKey.association: '_Dim' goes on the dimension key field in the cube@Analytics.dataCategory: #DIMENSION + @ObjectModel: { representativeKey: '<key>', supportedCapabilities: [ #ANALYTICAL_DIMENSION ], modelingPattern: #ANALYTICAL_DIMENSION }@ObjectModel.dataCategory: #TEXT (the ObjectModel. namespace — NOT @Analytics.dataCategory) + @Semantics.text: true + @Semantics.language: true(The text view stands alone — it declares no association, so it carries no @ObjectModel.foreignKey.association. The link is one-directional: the dimension associates to this text view via _Text + @ObjectModel.text.association, not the other way round.)
Composition rules (enforce before writing):
@Aggregation.default).@Semantics.amount.currencyCode + the currency field is projected; each quantity measure has @Semantics.quantity.unitOfMeasure + the unit field is projected.[1..1] and the dimension key field carries @ObjectModel.foreignKey.association.representativeKey.@ObjectModel.dataCategory: #TEXT + @Semantics.text/language.Show the user the model tree before writing:
On confirmation, create all views in one batch with deferred activation so cross-references resolve together:
Order the array dependencies-first (text → dimension → cube) so that even if activateAtEnd falls back to per-object activation on an older release, the chain still resolves. With activateAtEnd: true, ARC-1 writes all three as inactive drafts and fires one terminal activation over the whole graph.
If batch_create with activateAtEnd is not honored (older release), fall back to: create each object with SAPWrite(action="create", ...) then a single SAPActivate(objects=[...]) over all created objects.
Confirm the cube is live and its dimension association + measures resolved.
The cube is the foundation; the query is what end users consume. Offer:
"Want me to generate an analytical query (the consumable KPI view) on top of this cube? →
generate-cds-analytical-query"
| Error | Cause | Fix |
|---|---|---|
| Cube has no measure | All numeric fields treated as dimensions | Add @Aggregation.default: #SUM to at least one numeric field |
| Association cardinality error | Cube→dimension not [1..1] | Change cardinality to [1..1] |
foreignKey.association placement | Annotation on the association instead of the key field | Move it to the dimension key field in the cube |
| Text view rejected | Used @Analytics.dataCategory instead of @ObjectModel.dataCategory for #TEXT | Text views use the ObjectModel namespace |
| Cross-reference unresolved | Created objects activated individually before siblings existed | Use batch_create with activateAtEnd: true |
| Representative key missing | Dimension has no representativeKey | Add @ObjectModel.representativeKey |
#DIMENSION views). Extended = also generate new ZI_*_DIM + ZI_*_TXT and accept a DDIC table as the fact source (Step 1b).generate-cds-analytical-query).generate-rap-service.I_CalendarDate, I_Country, I_Currency) instead of regenerating them — SAPSearch in Step 2.@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Sales Cube'
@Analytics: {
dataCategory: #CUBE,
internalName: #LOCAL
}
@ObjectModel: {
supportedCapabilities: [ #ANALYTICAL_PROVIDER ],
modelingPattern: #ANALYTICAL_CUBE
}
define view entity ZI_Sales_Cube
as select from zsales_data
association [1..1] to ZI_Region_Dim as _Region
on _Region.RegionId = $projection.RegionId
{
@ObjectModel.foreignKey.association: '_Region'
key region_id as RegionId,
key calyear as CalendarYear,
@Semantics.amount.currencyCode: 'CurrencyCode'
@Aggregation.default: #SUM
amount as SalesAmount,
currency as CurrencyCode,
@Aggregation.default: #SUM
@EndUserText.label: 'Number of Records'
abap.int8'1' as RecordCount,
_Region
}@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Region Dimension'
@Analytics.dataCategory: #DIMENSION
@ObjectModel: {
representativeKey: 'RegionId',
supportedCapabilities: [ #ANALYTICAL_DIMENSION ],
modelingPattern: #ANALYTICAL_DIMENSION
}
define view entity ZI_Region_Dim
as select from zregion
association [0..1] to ZI_Region_Txt as _Text
on _Text.RegionId = $projection.RegionId
{
@ObjectModel.text.association: '_Text'
key region_id as RegionId,
_Text
}@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Region - Text'
@ObjectModel.dataCategory: #TEXT
@ObjectModel.representativeKey: 'RegionId'
define view entity ZI_Region_Txt
as select from zregion_t
{
key region_id as RegionId,
@Semantics.language: true
key spras as Language,
@Semantics.text: true
regiontext as RegionName
}Cube: ZI_Sales_Cube (from zsales_data)
Measures: SalesAmount (SUM, currency CurrencyCode), RecordCount (SUM)
Dimensions:
ZI_Region_Dim (new) → ZI_Region_Txt (new)SAPWrite(action="batch_create", activateAtEnd=true, objects=[
{ type: "DDLS", name: "ZI_Region_Txt", source: "<text_ddl>", package: "$TMP" },
{ type: "DDLS", name: "ZI_Region_Dim", source: "<dimension_ddl>", package: "$TMP" },
{ type: "DDLS", name: "ZI_Sales_Cube", source: "<cube_ddl>", package: "$TMP" }
])SAPRead(type="DDLS", name="ZI_Sales_Cube", include="elements")