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.
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:
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.
In total I have following RAP BOs for my complete example:
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.
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).
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:
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:
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.
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":
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:
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.
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:
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:
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.
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:
Note the green checkmark button under Log, meaning we do have an application log for this job. Let's click on it:
And lo and behold, our log exactly as we coded it, linked to our just scheduled application job.
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:
We can clearly see our stock values, now let's add a button to run the restocking in background.
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;
}
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.
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).
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...
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.
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.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
User | Count |
---|---|
58 | |
20 | |
11 | |
11 | |
7 | |
7 | |
6 | |
6 | |
6 | |
4 |