Technology Blog Posts by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
mlauber
Product and Topic Expert
Product and Topic Expert
5,128

Running certain functionality in background is a common requirement for ERP systems and no news for SAP. But with the increased importance of Clean Core and ABAP Cloud, how do we do it now? If you know a little about clean core, then you know that an se38 ABAP report, that is then scheduled in background, is not clean core, so that's not the answer. But does that mean we can schedule Fiori apps in background? Well, not exactly, but sort of 😊. Let me explain.

 

Background and SAP Fiori

SAP Fiori is our design system at SAP, coming with guidelines, principals, buildings blocks and much, much, more. But in the end, the result of Fiori is visual; when you open a Fiori app, you see the web-based UI on your device that is based on Fiori. Thus, Fiori in itself, cannot "run in the background". But we can give background running capabilities to Fiori: either by adding it directly to a Fiori app, for example giving it a button a user can click, which then triggers certain functionality to run in the background. Or we can simply design a background job (now called Application Job Template) which we can run or schedule to run in the background via the SAP delivered Fiori app Application Jobs (F1240). So in that sense we can say yes, we can have background jobs combined with Fiori apps.

If you are still confused, and no worries if you are, I completely understand, let me illustrate this by making an example for both options:

  • Creating an Application Job Template that can be run/scheduled in the Application Jobs Fiori app, which allows us to run "any ABAP code" in background. 
  • A RAP based Fiori app, in which a user can click a button to schedule a background job (using the Application Job Template as well)

 

1 RAP business object

First of, I need my RAP BO. Instead of using our beloved flight scenario, I opted to create a full custom example on my own. I may not go into full detail on every aspect of the RAP BO itself, as this blog focuses on the background running capability, so if you wonder anything, feel free to ask in the comments.

1.1 RAP BO data model

In total I have following RAP BOs for my complete example:

  • ZZ1_Author a Custom Business Object built in Custom Business Object Fiori app containing name, birthdate and other fields for an author.
  • ZR_Books containing simple fields such as the title of a book, number of pages, a price and more. And of course every book needs an author, so we associate ZZ1_Author (I didn't do parent-child composite relationship here for simpleness sake - if your scenario has parent-child relationship, everything will still work)
  • ZR_Bookstores for my imaginary bookstores that sells ZR_Books. Again, for simpleness sake we just have fields for the bookstore itself, such as its name and address.
  • ZR_BookstoreStockTP this now is my main BO combing bookstores and books. Please note, create/change/delete bookstores is only done with ZR_Bookstores and create/change/delete books with ZR_Books (I have behavior definitions with draft for both of them, projection views and finally OData V4 services, with simple Fiori Elements apps, one for each). The ZR_BookstoreStockTP BO as said combines these and also adds two stock values; one for how many of a certain book are available in the store currently, in shelves or on display, and how many of a certain book are in storage of that bookstore. To be perfectly transparent here, you could just use ZR_Bookstores as the main TP BO, having books as composite child and including the stock values that way. But these BOs have been used for other demo and use-cases by me, so I opted to reuse them as they are. So in conclusion, build your BO in the way it makes most sense.  Back to ZR_BookstoreStockTP; we also have behavior definition, projections and finally an OData V4 service for my "Manage Bookstores" Fiori elements app.

Now if my stock gets low for some books in one of my stores, in my "Manage Bookstores" app I want to schedule a background job for creating new purchase orders for restocking (for demo purpose and because my books aren't real products in the system for which I can create a PO, I will simply increase the stock, but I hope you get the idea). Now before we get into how this is done on the BO behavior, we first need to prepare this application job template.

 

2 Setup custom Application Job Template

First of, we need to create a new ABAP Cloud class in ADT. Now we already have a slight difference depending on your current system. Since Public Cloud release 2502, there is interface IF_APJ_RT_RUN available. This will be made available with the next full release of 2025 for private cloud and on-premise. So if you are not on public cloud, for now use 2 interfaces instead: IF_APJ_DT_EXEC_OBJECT and IF_APJ_RT_EXEC_OBJECT. If your Tier 1 development package doesn't default ABAP for Cloud Development language version, be sure to switch it for your class to be clean core compliant. With the interface(s) added to your class, we have 2 methods to implement (my demo system is on-premise so you will continue to see that version, but everything should work the same/very similar should you be on 2502).

2.1 Define job parameters

First of we need to define the possible parameters that could be entered by the Fiori app, or when we schedule the job via the Application Jobs Fiori app. Think of it as the selection-screen you had for the se38 report. It also works in the same way as we need to define either select-options or parameters. Below is my code as an example for the bookstore restocking scenario.

  method if_apj_dt_exec_object~get_parameters.

    et_parameter_def = value #(
      ( selname = 'P_STORID' kind = if_apj_dt_exec_object=>parameter
        component_type = 'ZBOOKSTORE_ID'  length = 3
        changeable_ind = abap_true mandatory_ind = abap_true )
      ( selname = 'S_BOOKID' kind = if_apj_dt_exec_object=>select_option
        component_type = 'ZBOOK_ID'       length = 6
        changeable_ind = abap_true mandatory_ind = abap_true )
      ( selname = 'P_REORQT' kind = if_apj_dt_exec_object=>parameter
        datatype = 'I' length = 3 param_text = 'Reorder Qty'
        changeable_ind = abap_true mandatory_ind = abap_true )
      ( selname = 'P_SIMUL'  kind = if_apj_dt_exec_object=>parameter 
        datatype = 'C' length = 1 param_text = 'Simulate Only'
        changeable_ind = abap_true checkbox_ind  = abap_true )
    ).

    " Return default parameter values, if any
    et_parameter_val = value #(
      ( selname = 'P_REORQT' kind = if_apj_dt_exec_object=>parameter 
        sign = 'I' option = 'EQ' low = '10' )
    ).

  endmethod.

Few comments on above:

  • The names of the parameters can be max 8 letters, just like selection-screens.
  • When not using component_type, give datatype (max 4 letters), length and label text
  • The 2nd part, returning default values. is optional
  • In the older version as I'm using, if you want your job parameters to offer a search help in the final Fiori app for scheduling your job, it must be done via component_type field, which points to a DDIC data element, with a DDIC search help on it. I have done this data element and search-help assignment for my bookstore and my book. Now, in the newer version, we will be able to enter CDS value help views. But if you are not yet on 2502 as myself, my recommendation is this:
    • Create a CDS value help as per usual practice (this you can then directly assign in the 2502 version as CDS value help view)
    • Before 2502, next also create a DDIC value help, using the CDS value help as selection method: refer to the example below
      mlauber_0-1744276692365.png

2.2 Code the job logic

Just like in an se38 report, all the logic that the job needs to do is coded into the 2nd, the EXECUTE method. It is highly recommended to also work with application logs, so that potential troubleshooting gets easy. The application log, when existing, is displayed in the Application Jobs Fiori app.
Now let's get to the meat of things. Here is my bookstore restocking code, which uses of course my RAP BO to do the restocking (or, at this point you could create real purchase orders or requisitions or whatever your process may be. You can also do as many checks as you need etc. etc. - write your ABAP code, but clean and in ABAP Cloud). Now don't get scared, but this code is quite a big bigger. I'll explain 🙂

method if_apj_rt_exec_object~execute.
    data: lrt_book          type range of zbook_id,
          lrs_book          like line of lrt_book,
          lv_bookstore      type zbookstore_id,
          lv_reorder_qty    type zbooks_in_stock,
          lv_simulate       type c length 1,
          lt_update         type table for update zr_bookstorestocktp\\bookstorestock,
          lt_create         type table for create zr_bookstorestocktp\\bookstorestock,
          ls_update         type structure for update zr_bookstorestocktp\\bookstorestock,
          ls_create         type structure for create zr_bookstorestocktp\\bookstorestock,
          lt_reported       type response for reported early zr_bookstorestocktp,
          lt_failed         type response for failed early zr_bookstorestocktp,
          ls_bookstore_book type zi_bookstorestock,
          lv_create         type c length 1.

    try.
        " handle log
        if sy-batch = abap_true.
          " if we are running in background, we create application log
          go_log = cl_bali_log=>create_with_header(
            header = cl_bali_header_setter=>create( 
              object    = 'ZBOOKSTORE_01_LOG'
              subobject = 'ZBOOKSTORE_01_SUB' ) ).
          add_text_to_log( 'start job' ).
          final(lv_log_handle) = go_log->get_handle( ).
          add_text_to_log( |log handle: { lv_log_handle }| ).
        else.
          add_text_to_log( 'start job' ).
          add_text_to_log( |foreground run, no log created| ).
        endif.

        " Going through job parameter values
        loop at it_parameters into data(ls_parameter).
          case ls_parameter-selname.
            when 'S_BOOKID'.
              append value #( sign   = ls_parameter-sign
                              option = ls_parameter-option
                              low    = conv #( ls_parameter-low  )
                              high   = conv #( ls_parameter-high ) ) 
                to lrt_book.
            when 'P_STORID'.
              lv_bookstore = conv #( ls_parameter-low ).
            when 'P_REORQT'.
              lv_reorder_qty = conv #( ls_parameter-low ).
            when 'P_SIMUL'.
              lv_simulate = ls_parameter-low.
          endcase.
        endloop.

        " get bookstore for which reorder was requested
        select single * from zr_bookstores
          where bookstoreid = _bookstore
          into (ls_bookstore).
        if sy-subrc <> 0.
          add_text_to_log( |bookstore not found with id: { lv_bookstore }| ).
        else.
          " go through books...
          loop at lrt_book into lrs_book.
            clear: lv_create, ls_bookstore_book, ls_update, ls_create.
            " check if bookstore already has the current book
            if lrs_book-option = 'EQ'. " only handling EQ!
              add_text_to_log( |processing book with id: { lrs_book-low }| ).
              select single * from zi_bookstorestock
                where bookstoreid = _bookstore
                  and bookid      = _book-low
                into _bookstore_book.
              if sy-subrc <> 0.
                " book is not yet in bookstore, check if book exists...
                select single  from zi_books
                  where bookid = _book-low
                  into (lv_exists).
                if sy-subrc = 0.
                  " book needs to be added (created) to bookstore
                  lv_create = abap_true.
                  ls_bookstore_book = value #( bookstoreid = lv_bookstore
                    bookid = lrs_book-low booksinstore = 0 booksinstock = 0 ).
                else.
                  add_text_to_log( |book does not exit and is ignored: { lrs_book-low }| ).
                  continue.
                endif.
              endif.
              add_text_to_log( |stock qty. before: { ls_bookstore_book-booksinstock }| ).
              ls_bookstore_book-booksinstock += lv_reorder_qty.
              add_text_to_log( |stock qty. after: { ls_bookstore_book-booksinstock }| ).
              " set if we create or update the bookstore stock RAP BO
              if lv_create = abap_true.
                ls_create = corresponding #( ls_bookstore_book ).
                append ls_create to lt_create.
              else.
                ls_update = corresponding #( ls_bookstore_book ).
                append ls_update to lt_update.
              endif.
            endif.
          endloop.
          if sy-subrc <> 0.
            add_text_to_log( 'no books to reorder specified' ).
          endif.
        endif.

        " UPDATE or SIMULATION ----------------------------------------------
        if lv_simulate = abap_true.
          add_text_to_log( 'SIMULATION only, no changes saved' ).
        else.
          " change data -----------------------------------------------------
          if lt_update is not initial.
            modify entities of zr_bookstorestocktp
                 entity bookstorestock
                   update fields ( booksinstock ) with lt_update
                reported data(update_reported)
                failed data(update_failed).
            if update_failed is initial.
              " commit change
              commit entities response of zr_bookstorestocktp
                reported data(commit_reported1)
                failed data(commit_failed1).
              " set errors if commit failed
              lt_reported = corresponding #( deep commit_reported1 ).
              lt_failed = corresponding #( deep commit_failed1 ).
            else.
              " set error
              lt_reported = corresponding #( update_reported ).
              lt_failed = corresponding #( update_failed ).
            endif.
          endif.
          " create data -----------------------------------------------------
          if lt_create is not initial.
            modify entities of zr_bookstorestocktp
                 entity bookstorestock
                   create
                   fields ( bookstoreid bookid booksinstock booksinstore )
                auto fill cid with lt_create 
                reported data(create_reported)
                failed data(create_failed).
            if create_failed is initial.
              " commit created
              commit entities response of zr_bookstorestocktp
                reported data(commit_reported2)
                failed data(commit_failed2).
              " set errors if commit failed
              lt_reported = corresponding #( deep commit_reported2 ).
              lt_failed = corresponding #( deep commit_failed2 ).
            else.
              " set error
              lt_reported = corresponding #( create_reported ).
              lt_failed = corresponding #( create_failed ).
            endif.
          endif.
        endif.

        if lt_failed is initial.
          " SUCCESS -------------------------------
          add_text_to_log( |commit success, nothing in failed| ).

        else.
          " ERRORS --------------------------------
          loop at lt_failed-bookstorestock into data(ls_failed).
            add_text_to_log( |failed to modify bookstore { ls_failed-bookstoreid } with book { ls_failed-bookid }| ).
            add_text_to_log( |reason: { ls_failed-%fail-cause }| ).
          endloop.
        endif.

        add_text_to_log( |job finished| ).

      catch cx_bali_runtime into data(lx_bali_exception).

        data(lv_log_exception) = lx_bali_exception->get_text(  ).
        raise exception type cx_apj_rt_content
          exporting
            previous = lx_bali_exception.

    endtry.

  endmethod.

And here the comments for above, going top to bottom:

  • We start by handling the application log. Now because I wanted to test this class in foreground running, I added foreground support, but this is of course not needed. We have the instance attribute go_log which is a reference to if_bali_log. In order to create the object, we must enter an application log object and sub-object, so a prerequisite for this, is that these are already created in your system. This can simply be done in ADT, via New - Other - search for "Application Log Object". Here a screenshot of mine:
    mlauber_1-1744278536107.png
  • Next we loop over all parameters which were passed into the background job run, and filling internal tables for further checking/selecting data.
  • Now we start using the RAP BO for checks, such as reading the bookstore and also all books are checked if they already exist. This job cannot create new books, but it can add an existing book to an existing bookstore, even if that bookstore does not yet have that book. This is done during the lrt_books loop, when we read from the interface view zi_bookstorestock to check if the current looped book is already in the store. If not, we add everything into an EML create table, otherwise an EML update table.
  • Lastly, we check if the simulation checkbox is true (this is one of the job parameters - again not mandatory; you can create a job template without a simulation checkbox, but I added it), in which case we don't proceed with any change to our RAP BO. If this is not a simulation run, then we use EML to create and/or update ZR_BookstoreStockTP. All these EML are checked for fails and only if everything goes well, we commit our changes.
  • At the very end after all create/update has been completed, we check whether we had errors and add the errors into the application log. If we had no errors, we write a success message in the log.
  • Note that the full execution is basically in a try-catch-block. This is because the application job object may raise an exception, so we have to catch it.
  • You may noticed the method call add_text_to_log, which again is a simple method I built that allows me to handle both foreground and background log. Here the code for that:
  method add_text_to_log.
    " running in background for foreground...
    if sy-batch = abap_true.
      " background - create text object for log
      data(o_go_log_free_text) =
        cl_bali_free_text_setter=>create(
          severity = if_bali_constants=>c_severity_status
          text = i_text ).
      o_go_log_free_text->set_detail_level( detail_level = '1' ).
      " add text object to log
      go_log->add_item( item = o_go_log_free_text ).
      " save log
      cl_bali_log_db=>get_instance( )->save_log( 
        log = go_log
        assign_to_current_appl_job = abap_true ).
    else.
      " foreground - simply add text to instace attribute, which can be fetched by caller program
      gs_fglog-no += 1.
      gs_fglog-text = i_text.
      append gs_fglog to gt_fglog.
    endif.
  endmethod.

That's is! Logic and coding part completed. But how can we now trigger this code to run in the background? Now we get into what we call Application Job Template.

2.3 Application Job Catalog Entry and Template

Creating the APJ class is a must, before we can create the actual Application Job Template. And the job template requires a Job Catalog Entry. All is again done in ADT. Via File - New - Other - and then search for "application job":

mlauber_0-1744617587183.png

We start with Application Job Catalog Entry. Here we must now enter the ABAP class, which has the EXECUTE method. Now depending on if your are in public cloud or on-premise/private, the next screen will look quite a bit different. I'll showcase a screenshot of both for reference:

  • On-premise/private cloud - what you see when you double click the job catalog entry in ADT:
    mlauber_2-1744617826367.png
  • On-premise/private cloud - what you see when you right-click the job catalog entry in ADT and choose Open With - SAP GUI:
    mlauber_3-1744617913160.png

     

  • Public cloud:
    job cat entry.jpg

For this demo, this is all we do here: we create the job catalog entry and give our ABAP class and we are done. At this point you would assign CDS value helps on parameters as shown in the public cloud screenshot, but as mentioned, this is not yet available before 2502.

The 2nd step is the Application Job Template, which then can be used in Fiori app Application Jobs. As before, in ADT choose New - File - Other and this time choose "Application Job Template". Here now we give the Application Job Catalog Entry we just created, and that's it. If you like, you can default parameter values here. This is only taken into account if you create a new "job" via Application Jobs Fiori app, where these parameters are then suggested as default, but can be changed. If we schedule the background run programmatically, no parameters are set by default.

With this we are now ready. We can already schedule a background job in Application Jobs Fiori app and we can code an action for our RAP BO.

All we've discussed so far, you may also refer to SAP Help: Application Jobs | SAP Help Portal and Working with Application Job Objects | SAP Help Portal.

 

3 Scheduling/running in background

3.1 Using Application Jobs Fiori app (to run immediately or schedule)

Let's start with the simpler option; scheduling our bookstore restocking to run in the background, via Application Jobs Fiori app. Maybe a new book has just come out and we know for the next five weeks we want to reorder that book once a week. Go to your Fiori Launchpad and make sure you have a role with access to the app. Open Application Jobs app. From the start screen, select Create button. A wizard opens and it always defaults the alphabetically first Application Job Template. Clear the field and open the search help and find the Application Job Template we created in step 2. This is mine:

mlauber_4-1744619638308.png

Optionally, you can rename the current Job we are creating, such as weekly restock or something. Click on Step 2 to continue the wizard.

Here you can enter the recurring pattern, just like you would for se38 reports. Be sure to setup bigger jobs with heavy processing logic to run outside of office hours, as per usual practice. For now I picked "start immediately" to showcase that my application job template is running:

mlauber_5-1744619946778.png

Continue by clicking Step 3. Here finally we enter the parameters and if you defined value helps for your parameters, this should be user friendly to use.

mlauber_6-1744620187794.png

Lastly when ready, click on Schedule.

Now because I told my job to start immediately, I can see the result of the first background run:

mlauber_7-1744620271205.png

Note the green checkmark button under Log, meaning we do have an application log for this job. Let's click on it:

mlauber_8-1744620341339.png

And lo and behold, our log exactly as we coded it, linked to our just scheduled application job.

3.2 Running background job from our own custom, RAP based, Fiori app

Now that the application job template is ready, we can also schedule a background job from a Fiori app. I will be using my RAP BO as explained in step 1. Now just as a little information, this is how my Fiori app looks like for ZR_BookstoreStockTP:

mlauber_9-1744621824816.png

We can clearly see our stock values, now let's add a button to run the restocking in background.

3.2.1 Abstract CDS entity for action parameters

I create a super simple abstract entity, containing all parameters I need to run the background job:

@EndUserText.label: 'Abstract entity to reorder books'
define root abstract entity ZD_Reorder_Books
{
    .defaultValue : '10'
    @EndUserText.label: 'Quantity to order'
    ReorderQty : zbooks_in_stock;
}

Secondly, I have a behavior for the same abstract entity, to ensure mandatory parameters:

abstract;
define behavior for ZD_Reorder_Books //alias <alias_name>
{
  field ( mandatory ) ReorderQty;
}

3.2.2 Action in behavior definition

Next we add the action to our RAP BO behavior (I'm only copying the action line here, not the whole behavior):

action (lock:none) reorderBooks parameter ZD_Reorder_Books;

Few comments: the (lock:none) is needed as I will run this action for all selected instances at once, instead of per instance (I explain that further on  the UI annotation part 3.2.3). Otherwise, quite straightforward action with input parameter.

3.2.3 UI annotation to display action button (and control how it calls the RAP framework)

Complicated title, small thing. First of, my action is an instance action (because I did not define it as a static action in the behavior definition) and it's also not a factory action, so it requires that in the result list in Fiori, at least 1 instance is selected through the checkboxes, for the action to run. Now to display the button in the table header, we need to annotate it at the first table field:

  @ui: { 
    lineItem: [ 
      { position: 10, importance: #HIGH },
      { type: #FOR_ACTION, dataAction: 'addBook', label: 'Add Book to Bookstore' },
      { type: #FOR_ACTION, dataAction: 'reorderBooks', label: 'Reorder Books', invocationGrouping: #CHANGE_SET },
      { type: #FOR_ACTION, dataAction: 'setInventory', label: 'Adjust Store Inventory', inline: true } 
    ]
 }
  BookstoreId;

The 2nd #FOR_ACTION is our reorderBooks action we just added to the behavior definition.  With this, a button with label "Reorder Books" will now be displayed on the table.

Secondly, I defined invocationGrouping: #CHANGE_SET.  This means that when more than 1 instance is selected when the action button is pressed, that instead of calling the action once per instance, the action groups all instances into one single call. That is why I had to define the (lock:none) in the behavior definition, because otherwise all instances would be locked by the time the action runs. Meaning I need to take care of any potential locking (which is not needed in this case, as we will schedule a background job).

3.2.4 Action behavior implementation

The 2nd to last step is the coding of our action, of course in the behavior implementation class. Here is mine:

  method reorderbooks.
    data:
      lv_bookstore         type zbookstore_id,
      lv_stop              type abap_bool,
      ls_job_parameter     type cl_apj_rt_api=>ty_job_parameter_value,
      ls_book_job_param    type cl_apj_rt_api=>ty_job_parameter_value,
      ls_value_range       type cl_apj_rt_api=>ty_value_range.

    check keys is not initial.
    " reset global static job parameter table
    zbp_r_bookstorestocktp=>gt_job_parameters = value #( ).

    " go through all selected books (and bookstore)
    loop at keys into data(ls_key).
      if lv_bookstore is initial.
        " set bookstore (ONCE) --------------------------------
        lv_bookstore = ls_key-bookstoreid.
        ls_job_parameter-name = zcl_apj_test=>gc_para_store.
        ls_value_range-sign = 'I'.
        ls_value_range-option = 'EQ'.
        ls_value_range-low = lv_bookstore.
        append ls_value_range to ls_job_parameter-t_value.
        " add to global static table, used in save modified
        append ls_job_parameter to zbp_r_bookstorestocktp=>gt_job_parameters. 
        clear: ls_job_parameter, ls_value_range.

        " set reorder quantity (ONCE) -------------------------
        if ls_key-%param-reorderqty <= 0.
          lv_stop = abap_true.
          append value #( %tky = ls_key-%tky ) to failed-bookstorestock.
          append value #( %msg     = new_message_with_text(
               severity = ms-error
               text     = |Please set reorder quantity to 1 or higher| ) )
            to reported-bookstorestock.
        else.
          ls_job_parameter-name = zcl_apj_test=>gc_para_qty.
          ls_value_range-sign = 'I'.
          ls_value_range-option = 'EQ'.
          ls_value_range-low = conv #( ls_key-%param-reorderqty ).
          append ls_value_range to ls_job_parameter-t_value.
          " add to global static table, used in save modified
          append ls_job_parameter to zbp_r_bookstorestocktp=>gt_job_parameters.
          clear: ls_job_parameter, ls_value_range.
        endif.

      elseif lv_bookstore <> ls_key-bookstoreid.
        " make sure we have only 1 bookstore selected
        lv_stop = abap_true.
        append value #( %tky = ls_key-%tky ) to failed-bookstorestock.
        append value #( %msg     = new_message_with_text(
             severity = ms-error
             text     = |Please select books for only 1 bookstore| ) )
          to reported-bookstorestock.
        exit.
      endif.

      " set selected book
      check lv_stop = abap_false.
      ls_value_range-sign = 'I'.
      ls_value_range-option = 'EQ'.
      ls_value_range-low = ls_key-bookid.
      append ls_value_range to ls_book_job_param-t_value.
      clear ls_value_range.
    endloop.
    check lv_stop = abap_false.

    " set all books parameter (range table filled in above loop)
    ls_book_job_param-name = zcl_apj_test=>gc_para_book.
    " add to global static table, used in save modified
    append ls_book_job_param to zbp_r_bookstorestocktp=>gt_job_parameters. 

  endmethod.

Now if you read above code, you will notice it doesn't do much yet; it performs some checks (only one bookstore selected) and eventually fills an application job parameters table which is a global class static variable, and that's it. The reason is that when we schedule the background job via cl_apj_rt_api, unfortunately as of right now, this class eventually calls a DB INSERT. For those more familiar with RAP know that it's not allowed to have any commits or hard DB changes during action processing. So we cannot actually schedule the background job from the the action itself, we can only prepare it because at this points we have all the instances that were selected on the Fiori UI. So to schedule the job, we need one final step...

3.2.5 Schedule background job programmatically from RAP additional save

We need "additional save". This can be included in a behavior definition. We simply add "with additional save" at the beginning of the behavior definition:

managed implementation in class zbp_r_bookstorestocktp unique;
strict ( 2 );

define behavior for ZR_BookstoreStockTP alias BookstoreStock
with additional save

Using ADT quick fix, you can add the corresponding method and class to your implementation class:

class lsc_zr_bookstorestocktp definition inheriting from cl_abap_behavior_saver.

  protected section.
    methods save_modified redefinition.

endclass.

We get a new local class that instead inherits from cl_abap_behavior_saver and has method save_modified.

Now finally in this method, we can call our application job template to run in the background:

  method save_modified.

    data:
      lv_job_template_name type cl_apj_rt_api=>ty_template_name 
        value 'ZBOOK_APJ_TEMPL',
      ls_job_parameter     type cl_apj_rt_api=>ty_job_parameter_value,
      ls_value_range       type cl_apj_rt_api=>ty_value_range,
      ls_job_start_info    type cl_apj_rt_api=>ty_start_info,
      lv_job_name          type cl_apj_rt_api=>ty_jobname,
      lv_job_count         type cl_apj_rt_api=>ty_jobcount.

    check zbp_r_bookstorestocktp=>gt_job_parameters is not initial.

    " get booktsore id from job parameters
    ls_job_parameter-name = zcl_apj_test=>gc_para_store.
    read table zbp_r_bookstorestocktp=>gt_job_parameters from ls_job_parameter using key name
      into ls_job_parameter.
    check sy-subrc = 0.
    read table ls_job_parameter-t_value index 1 into ls_value_range.
    check sy-subrc = 0.

    " reorder background job ------------------------------------------------
    try.
        ls_job_start_info-start_immediately = abap_true.
        " schedule job
        cl_apj_rt_api=>schedule_job(
          exporting
            iv_job_template_name   = lv_job_template_name
            iv_job_text            = |Restocking bookstore { ls_value_range-low }|
            is_start_info          = ls_job_start_info
            it_job_parameter_value = zbp_r_bookstorestocktp=>gt_job_parameters " built during action implemention
          importing
            ev_jobname             = lv_job_name
            ev_jobcount            = lv_job_count ).

        " success message
        append value #( %msg = new_message_with_text(
             severity = ms-success
             text = |Reorder job scheduled (job name: { lv_job_name })| ) )
          to reported-bookstorestock.

      catch cx_apj_rt into data(lx_job_scheduling_error).
        append value #( %msg = new_message(
                             id = 'ZBOOKSTORE'
                             number   = 000
                             severity = ms-error
                             v1 = lx_job_scheduling_error->get_longtext(  )
                             v2 = lx_job_scheduling_error->bapimsg-message
                        ) )
          to reported-bookstorestock.

      catch cx_root into data(lx_root_exception).
        append value #( %msg = new_message(
                            id = 'ZBOOKSTORE'
                            number   = 001
                            severity = if_abap_behv_message=>severity-error
                            v1 = lx_root_exception->get_longtext(  )
                        ) )
          to reported-bookstorestock.
    endtry.

  endmethod.

Very simple code really. We need of course to refer to an Application Job Template, the one we created. We define the job starting info and lastly the parameters, which we already prepared in he action implementation. 

And that's all. Now in my Fiori app I can select some instances and click on the button for Reorder and eventually a new background job is run, which I can also monitor in the Application Jobs Fiori app, including of course the log.

mlauber_10-1744624117797.png

 

Conclusion

Any kind of background capability can be provided clean core compliant via Fiori (even if no foreground application at all is needed - we can do the recurring background scheduling in Application Jobs Fiori app, as shown in step 3.1), so let's get away from our old-school se38 reports and instead work with RAP and Application Job Templates. We can also make use of SAP standard RAP, such as creating PO or other.

And do keep in mind the switch to the newer APJ version with 2025 release!

Hope this helps and let me know if there are any open questions regarding this topic.

20 Comments