ABAP CDS Views Series — Part 5: Virtual Elements, Parameters, and Session Variables for Dynamic Data Modeling

If you’ve been following this CDS Views series, you already know how to build layered architectures, optimize performance, and lock down access control with DCL. Now it’s time to tackle one of the most powerful—and most underused—capabilities in the CDS toolkit: virtual elements, input parameters, and session variables. These features allow you to build truly dynamic data models that adapt to context, user, and runtime conditions without touching a single line of classic ABAP report code.

In my experience, this is where CDS views go from being “just another database view” to becoming a legitimate application layer. Let me show you exactly how to use these features in real SAP S/4HANA environments, with practical code you can actually apply.

Why Static CDS Views Are Holding You Back

Here’s a scenario I’ve seen dozens of times. A team builds a beautiful CDS view for a Fiori analytical app. It works perfectly in the test system. Then someone asks: “Can we filter by the current user’s plant automatically?” Or: “Can we show the fiscal year based on the user’s company code calendar?”

Without the features we’re covering today, the answers to those questions push complexity into the Fiori frontend or into a custom ABAP class behind an OData service. That’s the wrong place for it. The data model should carry that logic.

This is the gap that virtual elements, input parameters, and session variables fill elegantly.

Understanding Input Parameters in CDS Views

Input parameters let you pass runtime values directly into a CDS view. Think of them as typed query parameters at the data model layer. They’re defined in the view header and consumed in the WHERE clause or in expressions.

Basic Input Parameter Syntax

Let’s start with a concrete example. Suppose you need a view that returns open purchase orders for a given fiscal year and controlling area.

@AbapCatalog.sqlViewName: 'ZV_OPEN_PO_PARAM'
@AbapCatalog.compiler.compareFilter: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Open POs by Fiscal Year'

define view ZC_OpenPurchaseOrders
  with parameters
    p_fiscal_year   : gjahr,
    p_kokrs         : kokrs

  as select from ekko as po
    inner join ekpo as poi
      on po.ebeln = poi.ebeln

  association [1..1] to I_CompanyCode as _CompanyCode
    on po.bukrs = _CompanyCode.CompanyCode

{
  key po.ebeln          as PurchaseOrder,
  key poi.ebelp         as PurchaseOrderItem,
      po.bukrs          as CompanyCode,
      po.lifnr          as Vendor,
      poi.netpr         as NetPrice,
      poi.waers         as Currency,
      po.gjahr          as FiscalYear,

      /* Only expose the association */
      _CompanyCode
}
where
  po.gjahr = $parameters.p_fiscal_year
  and po.kokrs = $parameters.p_kokrs
  and poi.elikz = ''
  and poi.loekz = ''

A few things worth noting here. The with parameters clause defines typed inputs—gjahr and kokrs are ABAP Dictionary types, so your parameters inherit all the domain validations automatically. The $parameters prefix is how you reference them inside the view body. This is clean, explicit, and immediately readable to any developer looking at the code.

Consuming Parameterized Views

When you consume a parameterized view in ABAP, you pass the values at runtime using the USING addition in Open SQL:

DATA: lt_open_po TYPE TABLE OF ZC_OpenPurchaseOrders.

SELECT *
  FROM ZC_OpenPurchaseOrders(
    p_fiscal_year = '2024',
    p_kokrs       = '1000'
  )
  INTO TABLE @lt_open_po.

Simple, typed, and the parameterization happens at the right layer. The Fiori OData layer or RAP service layer passes these parameters transparently to the underlying view—you’re not writing filter logic in multiple places.

Composing Parameterized Views

One thing that trips people up: when you use a parameterized view as a data source in another CDS view, you must pass parameters explicitly. Here’s how:

define view ZC_POAnalytics
  with parameters
    p_fiscal_year : gjahr,
    p_kokrs       : kokrs

  as select from ZC_OpenPurchaseOrders(
    p_fiscal_year = $parameters.p_fiscal_year,
    p_kokrs       = $parameters.p_kokrs
  ) as base

{
  base.PurchaseOrder,
  base.CompanyCode,
  base.NetPrice,
  base.Currency
}

You thread parameters through the view hierarchy explicitly. This is intentional—CDS doesn’t do implicit parameter forwarding, and that’s actually a good thing. It keeps the data flow visible and auditable.

Session Variables: Injecting Context Without Parameters

Input parameters are great when you want explicit caller control. But sometimes you want the view to automatically adapt to the runtime context—current user, current date, current language. That’s where CDS session variables shine.

Available Session Variables

SAP CDS supports a set of built-in session variables that you can reference directly inside view definitions:

  • $session.user — the current SAP user ID (SY-UNAME equivalent)
  • $session.client — the current client (SY-MANDT)
  • $session.system_language — the current logon language (SY-LANGU)
  • $session.system_date — the current application server date (SY-DATUM)

Let me show you a practical use case. Suppose you need a view that always returns only the records relevant to the logged-in user’s responsible plant—without any parameters, without any ABAP wrapper:

@AbapCatalog.sqlViewName: 'ZV_USER_NOTIFS'
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'My Notifications Today'

define view ZC_MyNotificationsToday
  as select from qmel as notif

{
  key notif.qmnum    as NotificationNumber,
      notif.qmart    as NotificationType,
      notif.matnr    as Material,
      notif.werks    as Plant,
      notif.ernam    as CreatedBy,
      notif.erdat    as CreationDate,
      notif.qmtxt    as NotificationText
}
where
  notif.ernam = $session.user
  and notif.erdat = $session.system_date
  and notif.qmsta <> 'I0009'  /* Exclude completed */

This view requires zero parameters. It self-contextualizes based on who’s running it and when. That’s the kind of design that makes Fiori app development significantly simpler—your OData service just exposes this view, and user-specific filtering is handled entirely in the data model.

Architect’s note: Use session variables for authorization-adjacent filtering (user, client, language) and input parameters for business-domain filtering (fiscal year, company code, plant). Mixing these purposes leads to confusion. Keep the intent clear.

Virtual Elements: Computed Columns Without Database Storage

Virtual elements are perhaps the most misunderstood feature in the CDS toolkit. They let you define a field in a CDS view that has no corresponding database column—instead, its value is computed at runtime via an ABAP method call. Think of them as a bridge between the declarative CDS world and imperative ABAP logic.

Virtual elements are particularly useful in RAP-based applications where you need computed fields like “criticality ratings,” formatted composite strings, or values derived from complex business logic that can’t be expressed in SQL.

Defining a Virtual Element

@AbapCatalog.sqlViewName: 'ZV_PO_RISK'
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'PO Risk Assessment View'

define root view entity ZC_POWithRiskAssessment
  as select from ekko

{
  key ebeln                          as PurchaseOrder,
      bukrs                          as CompanyCode,
      lifnr                          as Vendor,
      netwr                          as NetValue,
      waers                          as Currency,
      bedat                          as DocumentDate,

      /*
       * Virtual element: computed by ABAP class at runtime
       * The @ObjectModel annotations connect it to the implementation
       */
      @ObjectModel.virtualElement: true
      @ObjectModel.virtualElementCalculatedBy: 'ABAP:ZCL_PO_RISK_CALCULATOR'
      cast( 0 as abap.int1 )          as RiskLevel,

      @ObjectModel.virtualElement: true
      @ObjectModel.virtualElementCalculatedBy: 'ABAP:ZCL_PO_RISK_CALCULATOR'
      cast( ' ' as abap.char(30) )    as RiskDescription
}

Now let’s implement the calculator class. It must implement the interface IF_SADL_EXIT_CALC_ELEMENT_READ:

CLASS zcl_po_risk_calculator DEFINITION
  PUBLIC
  FINAL
  CREATE PUBLIC.

  PUBLIC SECTION.
    INTERFACES if_sadl_exit_calc_element_read.

ENDCLASS.

CLASS zcl_po_risk_calculator IMPLEMENTATION.

  METHOD if_sadl_exit_calc_element_read~calculate_elements.
    """
    @parameter it_original_data: Input - the raw CDS result set rows
    @parameter it_requested_elements: Which virtual elements are requested
    @parameter ct_calculated_data: Output - same rows with virtual fields populated
    """

    DATA: ls_original  TYPE LINE OF if_sadl_exit_calc_element_read=>tt_original_data,
          ls_calc      TYPE LINE OF if_sadl_exit_calc_element_read=>tt_calculated_data.

    FIELD-SYMBOLS: <ls_calc> LIKE LINE OF ct_calculated_data.

    ct_calculated_data = CORRESPONDING #( it_original_data ).

    LOOP AT ct_calculated_data ASSIGNING <ls_calc>.

      DATA(ls_data) = CORRESPONDING zc_powithriskassessment( <ls_calc> ).

      """ Compute risk level based on net value thresholds """
      DATA(lv_risk_level) = SWITCH abap.int1(
        ls_data-netvalue
        WHEN BETWEEN 0 AND 9999    THEN 1  " Low
        WHEN BETWEEN 10000 AND 99999 THEN 2  " Medium
        ELSE                             3   " High
      ).

      DATA(lv_risk_description) = SWITCH abap.char30(
        lv_risk_level
        WHEN 1 THEN 'Low Risk'
        WHEN 2 THEN 'Medium Risk'
        WHEN 3 THEN 'High Risk - Review Required'
      ).

      <ls_calc>-risklevel       = lv_risk_level.
      <ls_calc>-riskdescription = lv_risk_description.

    ENDLOOP.

  ENDMETHOD.

ENDCLASS.

A couple of important things to keep in mind here. Virtual elements are calculated after the database query executes—they run on the application server. This means they can’t be used in WHERE clauses for filtering, and they don’t benefit from database indexes. Use them for display and formatting logic, not for filtering or aggregation.

Also, the SWITCH expression I used above with BETWEEN is simplified for illustration—in production, you’d typically drive this from Customizing table entries rather than hardcoded thresholds.

Combining All Three: A Real-World Pattern

Let’s bring these three concepts together in a realistic scenario. You’re building a quality notification dashboard for a plant manager. The view should:

  • Automatically scope to the current user
  • Accept a date range via input parameters
  • Compute a display-friendly status label via a virtual element
@AbapCatalog.sqlViewName: 'ZV_QM_DASH'
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'QM Dashboard - Plant Manager View'

define root view entity ZC_QMDashboard
  with parameters
    p_date_from : dats,
    p_date_to   : dats

  as select from qmel as notif

{
  key notif.qmnum  as NotificationNumber,
      notif.qmart  as NotificationType,
      notif.werks  as Plant,
      notif.matnr  as Material,
      notif.erdat  as CreationDate,
      notif.qmsta  as SystemStatus,
      notif.ernam  as CreatedBy,

      @ObjectModel.virtualElement: true
      @ObjectModel.virtualElementCalculatedBy: 'ABAP:ZCL_QM_STATUS_FORMATTER'
      cast( ' ' as abap.char(40) ) as StatusDescription,

      @ObjectModel.virtualElement: true
      @ObjectModel.virtualElementCalculatedBy: 'ABAP:ZCL_QM_STATUS_FORMATTER'
      cast( 0 as abap.int1 )       as StatusCriticality
}
where
  notif.ernam  = $session.user
  and notif.erdat >= $parameters.p_date_from
  and notif.erdat <= $parameters.p_date_to

This is a clean, self-describing view. Any developer reading it immediately understands: who it’s for (current user), what controls its scope (date parameters), and what computed display logic runs on top (status formatter). That clarity is architectural value.

Performance Considerations You Can’t Ignore

A few hard-won lessons before you ship this into production:

Input Parameters and Index Usage

Parameterized CDS views generate dynamic SQL at runtime. Make sure the columns referenced in your WHERE clause with parameters are backed by appropriate HANA indexes. Use the HANA SQL Analyzer or SAT (ABAP Trace) to verify execution plans. If your parameter-driven filter doesn’t hit an index, you’ll end up with full table scans at runtime—which can be painful on tables like EKKO or QMEL in large systems.

Virtual Element Overhead

Virtual elements run ABAP code for every row in the result set. If your view returns 50,000 rows, your calculator class is invoked for 50,000 records. Keep the logic in the calculator class lightweight. Move heavy computations to Customizing table lookups with buffering enabled, never to RFC calls or database reads inside the loop.

Session Variable Filtering and Pushdown

Session variables like $session.user are evaluated on the database layer in HANA—they get pushed down as query predicates. This is efficient. But make sure the column you’re filtering (ernam, for example) has adequate selectivity in your data distribution. A filter on ernam with a poorly distributed index can still result in expensive scans.

For deeper guidance on identifying and fixing these kinds of bottlenecks, check out the earlier article on SAP ABAP Performance Optimization: Identifying and Fixing Bottlenecks in Real-World Systems—a lot of those principles apply directly to CDS-backed OData services as well.

Access Control with Parameterized Views

One pattern worth calling out explicitly: when you use $session.user in a CDS view, you might think you’ve handled authorization. You haven’t—not fully. Session variable filtering is data scoping, not access control.

You still need a DCL (Data Control Language) object to enforce authorization objects. If you haven’t worked with CDS DCL before, the CDS Views Series Part 4: Advanced Annotations, Access Control and DCL in SAP S/4HANA covers this in depth. Use both: session variables for natural data scoping, DCL for formal authorization enforcement.

Quick Reference: When to Use Each Feature

Feature Best For Avoid When
Input Parameters Business domain filtering (fiscal year, plant, date range) Deriving values from user context automatically
Session Variables User context (current user, language, date) Business-domain filtering that callers should control
Virtual Elements Display logic, computed labels, criticality Filtering, aggregation, large result sets with heavy logic

Summary and Next Steps

Virtual elements, input parameters, and session variables are the features that separate a mature CDS view architecture from a basic one. When you combine them deliberately, you get a data model that is:

  • Context-aware: adapts to the current user and session
  • Parameterizable: accepts runtime business filters without ABAP boilerplate
  • Display-ready: exposes computed, UI-friendly fields alongside raw data

The next part of this series will focus on CDS-based analytical views and HANA-native aggregations—specifically how to design cube views and dimension views for embedded analytics dashboards, and when to use them versus CDS Projection Views for transactional scenarios. Stay tuned.

If you’re building RAP-based apps alongside these CDS views, don’t miss the ABAP RAP: A Senior Architect’s Guide to Building Modern Fiori Apps—the two topics complement each other closely.


What’s your experience with virtual elements in production? Have you hit performance walls with large result sets, or found creative uses for session variables I haven’t covered here? Drop your thoughts in the comments below—I read every one, and the best questions often become the next article.

Scroll to Top