Post

Reading BRF+ Decision Table Data via API in ABAP

Learn how to programmatically read BRF+ decision table data using the FDT API in ABAP. This comprehensive guide covers querying applications, accessing decision tables, and extracting table data into custom structures.

Reading BRF+ Decision Table Data via API in ABAP

SAP BRF+ (Business Rule Framework plus) provides a powerful framework for managing business rules and decision logic. While the UI tools are great for manual maintenance, there are scenarios where you need to programmatically access decision table data—for example, to sync data to external systems, create backups, or migrate decision tables to database tables for performance optimization.

In this post, I’ll show you how to use the BRF+ FDT (Formula and Decision Table) API to read decision table data programmatically.


🎯 Use Cases for Reading Decision Tables Programmatically

Before diving into the code, here are some common scenarios where reading decision table data via API is useful:

  • Data Migration: Export decision table content to database tables or external systems
  • Backup & Version Control: Create automated backups of business rules
  • Performance Optimization: Copy frequently accessed decision tables to optimized database tables
  • Integration: Share business rules with external applications
  • Auditing: Track and analyze decision table content changes

🏗️ Understanding the BRF+ API Structure

The BRF+ API is built around the Factory Design Pattern, accessed through the cl_fdt_factory class. The main components we’ll work with are:

  • Factory (if_fdt_factory): Entry point for all BRF+ operations
  • Query (if_fdt_query): Used to search for BRF+ objects
  • Decision Table (if_fdt_decision_table): Interface for accessing table data
  • Expression (if_fdt_expression): Base interface for BRF+ expressions including decision tables

📋 Step 1: Initialize the Factory

Every interaction with the BRF+ API starts with getting an instance of the factory:

1
2
3
DATA: factory TYPE REF TO if_fdt_factory.

factory = cl_fdt_factory=>get_instance( ).

This singleton instance provides access to all BRF+ query and manipulation operations.


🔍 Step 2: Find the Application ID

BRF+ organizes objects into applications. Before accessing a decision table, you need to find the application ID by name:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
" Get application ID by name
factory->get_query( )->get_ids(
  EXPORTING
    iv_name           = 'YOUR_APPLICATION_NAME'  " e.g., 'Z_PRICING_RULES'
    iv_object_type    = if_fdt_constants=>gc_object_type_application
    iv_deleted_option = cl_fdt_query=>if_fdt_query~gc_delopt_undeleted
  IMPORTING
    ets_object_id     = DATA(application_ids)
).

IF lines( application_ids ) NE 1.
  " Handle error: application not found or multiple results
  WRITE: / 'Application not found or ambiguous!'.
  RETURN.
ENDIF.

DATA(application_id) = application_ids[ 1 ].

The query parameters ensure that:

  • Only undeleted applications are returned
  • The exact application name is matched
  • We get the unique object ID for further operations

🎲 Step 3: Query for the Decision Table

Now that we have the application ID, we can search for a specific decision table within that application:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
DATA: selection      TYPE if_fdt_query=>s_selection,
      selections     TYPE if_fdt_query=>ts_selection,
      decision_tables TYPE if_fdt_query=>ts_name.

" Build query for application
selection-queryfield = if_fdt_admin_data_query=>gc_fn_application_id.
selection-sign       = 'I'.
selection-option     = 'EQ'.
selection-low        = application_id.
INSERT selection INTO TABLE selections.

" Filter by object type (Expression = Decision Table)
selection-queryfield = if_fdt_admin_data_query=>gc_fn_object_type.
selection-sign       = 'I'.
selection-option     = 'EQ'.
selection-low        = if_fdt_constants=>gc_object_type_expression.
INSERT selection INTO TABLE selections.

" Filter by decision table name
selection-queryfield = if_fdt_admin_data_query=>gc_fn_name.
selection-sign       = 'I'.
selection-option     = 'EQ'.
selection-low        = 'YOUR_DECISION_TABLE_NAME'.  " e.g., 'DT_PRICE_CALCULATION'
INSERT selection INTO TABLE selections.

" Execute query
factory->get_query( )->select_data(
  EXPORTING
    its_selection = selections
  IMPORTING
    eta_data      = decision_tables
).

IF lines( decision_tables ) NE 1.
  " Handle error: decision table not found
  WRITE: / 'Decision table not found!'.
  RETURN.
ENDIF.

DATA(decision_table_info) = decision_tables[ 1 ].

The selection criteria work like database SELECT statements, using sign/option/low/high patterns familiar from ABAP ranges.


📊 Step 4: Read the Decision Table Data

With the decision table ID in hand, we can now access its actual content:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
DATA: decision_table TYPE REF TO cl_fdt_decision_table,
      table_data     TYPE TABLE OF if_fdt_decision_table=>s_table_data
                     WITH NON-UNIQUE SORTED KEY row COMPONENTS row_no col_no.

TRY.
    " Get decision table instance
    decision_table ?= factory->get_expression( decision_table_info-id ).
    
    " Retrieve table data
    decision_table->if_fdt_decision_table~get_table_data(
      IMPORTING
        ets_data = DATA(raw_table_data)
    ).
    
    " Store in sorted table for easier processing
    LOOP AT raw_table_data ASSIGNING FIELD-SYMBOL(<raw_data>).
      INSERT <raw_data> INTO TABLE table_data.
    ENDLOOP.
    
  CATCH cx_fdt INTO DATA(exception).
    " Handle error
    WRITE: / 'Error reading decision table:', exception->get_text( ).
    RETURN.
ENDTRY.

The get_table_data method returns the decision table content as a flat structure where each cell is represented individually with its row and column position.


🔄 Step 5: Transform Data into Custom Structure

The raw table data from BRF+ needs to be transformed into a usable format. Here’s how to map it to a target database table or structure:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
DATA: target_table TYPE REF TO data,
      target_line  TYPE REF TO data.

FIELD-SYMBOLS: <target_table> TYPE STANDARD TABLE,
               <target_line>   TYPE any.

" Create dynamic table and line structures
CREATE DATA target_table TYPE TABLE OF ('YOUR_TARGET_TABLE').
ASSIGN target_table->* TO <target_table>.

CREATE DATA target_line TYPE ('YOUR_TARGET_TABLE').
ASSIGN target_line->* TO <target_line>.

DATA(current_row) = 1.

FIELD-SYMBOLS: <cell_value>   TYPE REF TO data,
               <target_field> TYPE any.

DO.
  CLEAR <target_line>.
  
  " Process all columns in current row
  LOOP AT table_data ASSIGNING FIELD-SYMBOL(<cell_data>) 
       USING KEY row WHERE row_no EQ current_row.
    
    " Extract the actual value from the cell
    IF <cell_data>-r_value IS NOT INITIAL.
      " Direct value reference
      ASSIGN <cell_data>-r_value TO <cell_value>.
      
    ELSEIF <cell_data>-ts_range IS NOT INITIAL.
      " Handle range values (e.g., single equality conditions)
      IF lines( <cell_data>-ts_range ) EQ 1.
        READ TABLE <cell_data>-ts_range ASSIGNING FIELD-SYMBOL(<range>) INDEX 1.
        
        " Validate that it's a simple equality or GE condition
        IF <range>-sign NE 'I' OR <range>-option NE 'EQ'.
          WRITE: / 'Invalid value type in decision table!'.
          RETURN.
        ENDIF.
        
        ASSIGN <range>-r_low_value TO <cell_value>.
      ELSE.
        WRITE: / 'Complex range values not supported!'.
        RETURN.
      ENDIF.
      
    ELSE.
      " Empty cell, skip
      CONTINUE.
    ENDIF.
    
    " Map to target structure field
    " Note: +1 offset if target table has MANDT field
    DATA(target_column) = <cell_data>-col_no + 1.
    ASSIGN COMPONENT target_column OF STRUCTURE <target_line> TO <target_field>.
    
    IF sy-subrc EQ 0.
      <target_field> = <cell_value>->*.
    ENDIF.
    
  ENDLOOP.
  
  " Check if we've processed all rows
  IF sy-subrc NE 0.
    EXIT.
  ENDIF.
  
  " Add line to result table
  APPEND <target_line> TO <target_table>.
  
  current_row += 1.
ENDDO.

This code handles the most common data types in decision tables. The key aspects are:

  • Column Mapping: BRF+ columns are mapped to structure components
  • Value Extraction: Handles both direct values and range conditions
  • MANDT Handling: Accounts for client field if present in target structure

💾 Step 6: Persist Data to Database

Once the data is transformed, you can persist it to a database table:

1
2
3
4
5
6
7
8
9
10
11
12
13
" Clear existing data
DELETE FROM ('YOUR_TARGET_TABLE').

" Insert transformed data
LOOP AT <target_table> ASSIGNING <target_line>.
  INSERT ('YOUR_TARGET_TABLE') FROM <target_line>.
ENDLOOP.

" Verify results
SELECT COUNT( * ) FROM ('YOUR_TARGET_TABLE') INTO DATA(record_count).

WRITE: / 'Processed', lines( <target_table> ), 'rows.',
       / 'Inserted', record_count, 'records into database.'.

For better performance with large datasets, consider using MODIFY with the entire table instead of individual inserts.


🎯 Complete Example: Reading and Syncing Decision Tables

Here’s a complete example that ties everything together:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
DATA: factory          TYPE REF TO if_fdt_factory,
      application_id   TYPE fdt_id,
      decision_table   TYPE REF TO cl_fdt_decision_table.

" Initialize factory
factory = cl_fdt_factory=>get_instance( ).

" Get application ID
factory->get_query( )->get_ids(
  EXPORTING
    iv_name           = 'Z_PRICING_APP'
    iv_object_type    = if_fdt_constants=>gc_object_type_application
    iv_deleted_option = cl_fdt_query=>if_fdt_query~gc_delopt_undeleted
  IMPORTING
    ets_object_id     = DATA(application_ids)
).

IF lines( application_ids ) NE 1.
  WRITE: / 'Application not found!'.
  RETURN.
ENDIF.

application_id = application_ids[ 1 ].

" Query decision table
DATA: selection  TYPE if_fdt_query=>s_selection,
      selections TYPE if_fdt_query=>ts_selection.

selection-queryfield = if_fdt_admin_data_query=>gc_fn_application_id.
selection-sign   = 'I'.
selection-option = 'EQ'.
selection-low    = application_id.
INSERT selection INTO TABLE selections.

selection-queryfield = if_fdt_admin_data_query=>gc_fn_object_type.
selection-sign   = 'I'.
selection-option = 'EQ'.
selection-low    = if_fdt_constants=>gc_object_type_expression.
INSERT selection INTO TABLE selections.

selection-queryfield = if_fdt_admin_data_query=>gc_fn_name.
selection-sign   = 'I'.
selection-option = 'EQ'.
selection-low    = 'DT_PRICING_RULES'.
INSERT selection INTO TABLE selections.

factory->get_query( )->select_data(
  EXPORTING
    its_selection = selections
  IMPORTING
    eta_data      = DATA(decision_tables)
).

IF lines( decision_tables ) NE 1.
  WRITE: / 'Decision table not found!'.
  RETURN.
ENDIF.

DATA(dt_info) = decision_tables[ 1 ].

" Read table data
TRY.
    decision_table ?= factory->get_expression( dt_info-id ).
    
    decision_table->if_fdt_decision_table~get_table_data(
      IMPORTING
        ets_data = DATA(raw_data)
    ).
    
    DATA: table_data TYPE TABLE OF if_fdt_decision_table=>s_table_data
                     WITH NON-UNIQUE SORTED KEY row COMPONENTS row_no col_no.
    
    LOOP AT raw_data ASSIGNING FIELD-SYMBOL(<raw>).
      INSERT <raw> INTO TABLE table_data.
    ENDLOOP.
    
    " Transform and process data
    DATA: target_table TYPE REF TO data,
          target_line  TYPE REF TO data.
    
    FIELD-SYMBOLS: <t_table> TYPE STANDARD TABLE,
                   <s_line>  TYPE any.
    
    CREATE DATA target_table TYPE TABLE OF ('Z_PRICING_TABLE').
    ASSIGN target_table->* TO <t_table>.
    
    CREATE DATA target_line TYPE ('Z_PRICING_TABLE').
    ASSIGN target_line->* TO <s_line>.
    
    DATA(row_number) = 1.
    
    FIELD-SYMBOLS: <cell_ref>    TYPE REF TO data,
                   <table_field> TYPE any.
    
    DO.
      CLEAR <s_line>.
      
      LOOP AT table_data ASSIGNING FIELD-SYMBOL(<cell>) 
           USING KEY row WHERE row_no EQ row_number.
        
        IF <cell>-r_value IS NOT INITIAL.
          ASSIGN <cell>-r_value TO <cell_ref>.
          
        ELSEIF <cell>-ts_range IS NOT INITIAL.
          IF lines( <cell>-ts_range ) EQ 1.
            READ TABLE <cell>-ts_range ASSIGNING FIELD-SYMBOL(<range>) INDEX 1.
            
            IF <range>-sign NE 'I' OR <range>-option NE 'EQ'.
              WRITE: / 'Invalid range condition!'.
              RETURN.
            ENDIF.
            
            ASSIGN <range>-r_low_value TO <cell_ref>.
          ELSE.
            WRITE: / 'Complex ranges not supported!'.
            RETURN.
          ENDIF.
          
        ELSE.
          CONTINUE.
        ENDIF.
        
        DATA(column_index) = <cell>-col_no + 1.
        ASSIGN COMPONENT column_index OF STRUCTURE <s_line> TO <table_field>.
        
        IF sy-subrc EQ 0.
          <table_field> = <cell_ref>->*.
        ENDIF.
        
      ENDLOOP.
      
      IF sy-subrc NE 0.
        EXIT.
      ENDIF.
      
      APPEND <s_line> TO <t_table>.
      row_number += 1.
      
    ENDDO.
    
    " Persist to database
    DELETE FROM z_pricing_table.
    
    LOOP AT <t_table> ASSIGNING <s_line>.
      INSERT z_pricing_table FROM <s_line>.
    ENDLOOP.
    
    SELECT COUNT( * ) FROM z_pricing_table INTO DATA(db_count).
    
    WRITE: / 'Successfully synced', lines( <t_table> ), 'rows to Z_PRICING_TABLE',
           / 'Database contains', db_count, 'records'.
    
  CATCH cx_fdt INTO DATA(exception).
    WRITE: / 'Error:', exception->get_text( ).
    RETURN.
ENDTRY.

⚠️ Important Considerations

Performance

  • Caching: Consider caching decision table data if accessed frequently
  • Batch Processing: For multiple tables, process them in batches
  • Database Operations: Use MODIFY instead of individual INSERT for better performance

Data Types

The code above handles the most common scenarios:

  • Direct values (r_value)
  • Simple range conditions (EQ)

For more complex scenarios, you may need to extend the logic to handle:

  • Multiple range conditions
  • Complex expressions
  • Nested decision tables

Authorization

Ensure that the user executing this code has appropriate authorizations:

  • S_DEVELOP: For accessing ABAP development objects
  • S_FDT_APP: For BRF+ application access
  • S_TABU_DIS: For database table access (if using dynamic table operations)

Error Handling

Always implement proper error handling:

  • Check for empty result sets
  • Handle BRF+ exceptions (cx_fdt)
  • Validate data integrity before persisting


🧠 Final Thoughts

Using the BRF+ FDT API provides powerful programmatic access to decision table data. The key steps are:

  1. Initialize the factory with cl_fdt_factory=>get_instance( )
  2. Query for the application and decision table using selections
  3. Retrieve table data with get_table_data( )
  4. Transform the row/column structure into your target format
  5. Persist or process the data as needed

This approach enables automation scenarios like synchronization, backup, migration, and integration that would be tedious or impossible through the UI alone.

The BRF+ API is extensive and offers many more capabilities beyond reading decision tables—including creating, modifying, and executing business rules programmatically. This foundation will help you explore those advanced scenarios as your requirements grow.

This post is licensed under CC BY 4.0 by the author.