Contents
Tell an ABAP developer "this should run at night" and for years the answer was the same: write a report, save a variant for the selection screen, schedule a job in SM36, monitor it in SM37. Side work that had to run after a save used CALL FUNCTION ... IN BACKGROUND TASK or STARTING NEW TASK.
In the ABAP Cloud language version, most of these paths are not released APIs. Two separate tools replace them, and the most important design decision is choosing correctly which one a task belongs to. The interface, class and method names in this article are based on SAP's BTP ABAP environment documentation and the official ABAP cheat sheet repository; these APIs keep evolving between releases, so check the documentation for your system's release too.
Why SM36 and SUBMIT Are Not Enough
In on-premise S/4HANA, SM36 still works for code written in classic ABAP. The issue is that under the clean core approach, new development is written with ABAP Cloud rules, and that code cannot use reports, SUBMIT or the classic job scheduling functions. This is less a restriction than a distinction:
- A scheduled job needs a calendar, a selection screen and a monitoring screen: "Every night at 02:00, post the approved expense reports."
- An asynchronous step is part of a transaction and should run only if that transaction succeeds: "When a report is approved, send the notification without making the user wait."
In the classic world, both were often solved with the same tool, a background job. In ABAP Cloud, they are two separate tools.
Decision Table
| Aspect | Application Jobs | bgPF |
|---|---|---|
| What starts it? | A schedule, a user (the "Application Jobs" app) or an API | The COMMIT WORK of a transaction |
| When does it run? | At the scheduled time; once or periodically | After the transaction is saved, queued; once |
| Parameters | Selection screen (public attributes of the class) and template | Attributes of the operation object (set in code) |
| Transactional consistency | The job is a transaction of its own | Tied to the starting transaction: on rollback the process never starts |
| Monitoring | The Application Jobs app and the application log attached to the job | The process monitor object and bgRFC monitoring tools |
| Typical work | Nightly posting, mass correction, periodic cleanup | Notification after a save, document generation, a slow side task |
Application Jobs: Four Parts
An application job consists of four objects:
- Job class: The code to run. For new development, the
IF_APJ_RT_RUNinterface is recommended. The olderIF_APJ_DT_EXEC_OBJECT+IF_APJ_RT_EXEC_OBJECTpair is still supported, but SAP's documentation says not to use it for new development. - Job catalog entry: Registers the class with the job framework and defines the parameters, groups and sections of the selection screen. Created in ADT via Other ABAP Repository Object → Application Jobs → Application Job Catalog Entry.
- Job template: Linked to the catalog entry and holds default parameter values. It is the counterpart of the classic variant.
- Authorization: In ABAP Cloud, additional authorization steps in ADT and in the administrator's launchpad are needed before the template can be scheduled from the "Application Jobs" Fiori app.
In the BTP ABAP environment, the job class must be in a customer-defined software component (not ZLOCAL) to be transportable. Application jobs can also be used in on-premise S/4HANA; check whether IF_APJ_RT_RUN is available in your system's release, and if not, build the same structure with the older interface pair.
The Job Class and Its Parameters
With IF_APJ_RT_RUN, the selection screen is not defined in a separate method: the class's public attributes become the parameters. Before calling EXECUTE, the framework fills these attributes with the values entered on the selection screen. Example: a job that posts approved expense reports to accounting.
CLASS zcl_exp_post_job DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_apj_rt_run.
TYPES ty_company_code_range TYPE RANGE OF zexp_report-company_code.
"! <p class="shorttext synchronized" lang="en">Company code</p>
DATA company_codes TYPE ty_company_code_range.
"! <p class="shorttext synchronized" lang="en">Test mode</p>
DATA test_mode TYPE abap_boolean VALUE abap_true.
PRIVATE SECTION.
METHODS post_report
IMPORTING report_uuid TYPE sysuuid_x16.
ENDCLASS.Rules from the documentation:
- The attribute must be public; constants and
READ-ONLYattributes cannot be parameters. The name is limited to 38 characters. - For a single-value field, the attribute must have an elementary type.
STRINGis allowed, but the framework carries at most 255 characters; longer values are cut off. - For multiple values or ranges, the attribute must be a ranges table (
TYPE RANGE OF). - The parameter title is taken from the attribute's ABAP Doc description (the
"!lines above). - Default values can be set three ways: in the attribute declaration (
VALUE abap_true), in the constructor, or in theFILL_ATTRIBUTE_DEFAULTSmethod of theIF_APJ_DT_DEFAULTSinterface. - For a date parameter, a dynamic value such as
@STARTDAYPLUSDAY@:03can be entered in the template.
Defaulting test_mode to abap_true is deliberate: if a new template is run by mistake for the first time, it changes nothing and only logs what it would do.
Errors and Logging
The signature of EXECUTE contains the exception CX_APJ_RT_CONTENT. According to SAP's documentation, when you raise it with a message, the framework catches it, creates a log containing the message and the source position, and the job gets the status Failed. Any other exception not handled inside EXECUTE leads to a runtime error (short dump). Catching unexpected errors as well and converting them into CX_APJ_RT_CONTENT therefore leaves a meaningful message for whoever monitors the job.
METHOD if_apj_rt_run~execute.
TRY.
DATA(log) = cl_bali_log=>create_with_header(
header = cl_bali_header_setter=>create( object = 'ZEXP'
subobject = 'POST' ) ).
SELECT ReportUUID, ReportID
FROM zi_expensereport
WHERE CompanyCode IN @company_codes
AND Status = 'A'
INTO TABLE @DATA(reports).
LOOP AT reports INTO DATA(report).
IF test_mode = abap_false.
post_report( report-ReportUUID ).
ENDIF.
log->add_item( item = cl_bali_free_text_setter=>create(
severity = if_bali_constants=>c_severity_status
text = COND #( WHEN test_mode = abap_true
THEN |Test: { report-ReportID } would be posted|
ELSE |{ report-ReportID } posted| ) ) ).
ENDLOOP.
cl_bali_log_db=>get_instance( )->save_log( log = log
assign_to_current_appl_job = abap_true ).
CATCH cx_bali_runtime INTO DATA(log_error).
RAISE EXCEPTION TYPE cx_apj_rt_content
MESSAGE ID 'ZEXP' NUMBER '001' WITH log_error->get_text( ).
ENDTRY.
ENDMETHOD.assign_to_current_appl_job = abap_true attaches the log to the job; it appears in the job details in the Application Jobs app. If the log must survive even when the job's work is rolled back, the save_log_2nd_db_connection method saves it through a separate database connection and commits immediately.
Two more notes:
- The job reads with the authorizations of the user it runs under. If
zi_expensereportin the example has a DCL role, the result is filtered for that user. Handle this deliberately for a technical job that has to process all company codes; see the CDS access control article for details. post_reportshould not write to the table directly. If the report's status is managed by a RAP business object, the job should change it through EML too (MODIFY ENTITIESandCOMMIT ENTITIES); that way the validations, determinations and locks of the UI also apply in the background job.
Scheduling a Job from Code
When code rather than a user needs to start a job, use the released class CL_APJ_RT_API. Its SCHEDULE_JOB method takes the template name, start information, periodicity and parameter values; methods such as GET_JOB_STATUS, FIND_JOBS_WITH_JCE and CANCEL_JOB monitor and cancel jobs.
TRY.
GET TIME STAMP FIELD DATA(now).
cl_apj_rt_api=>schedule_job(
EXPORTING
iv_job_template_name = 'ZEXP_POST_TEMPLATE'
iv_job_text = 'Expense posting'
is_start_info = VALUE #( timestamp = now )
is_scheduling_info = VALUE #( test_mode = abap_false
timezone = 'UTC' )
it_job_parameter_value = VALUE #(
( name = 'TEST_MODE'
t_value = VALUE #( ( sign = 'I' option = 'EQ' low = abap_false ) ) ) )
IMPORTING
ev_jobname = DATA(job_name)
ev_jobcount = DATA(job_count) ).
CATCH cx_apj_rt INTO DATA(schedule_error).
" write schedule_error->get_longtext( ) to the user or a log
ENDTRY.If no parameter table is passed, the template's values are used; if one is passed, only the parameters it contains override the template. For a periodic job, pass the period in is_scheduling_info (periodic_granularity, periodic_value) and the end condition in is_end_info. The method's mandatory and optional parameters can vary by release, so check the class documentation.
start_immediately cannot be used from within a RAP business object, because the underlying API performs an implicit COMMIT WORK. Schedule a job that should start right away with a timestamp (as above); it starts with a short delay depending on system load.bgPF: In the Background After a Save
The Background Processing Framework (bgPF) runs an application's methods in the background, asynchronously and reliably. It is built on bgRFC, supports transactional and queued processes, and is a standard part of ABAP Cloud. On-premise, it ships with S/4HANA 2023 and can be used after a short configuration.
Its key property is timing: the process is registered with save_for_execution, but runs only when the starting transaction completes with COMMIT WORK. If the transaction is rolled back, the process never starts. So the rule "if the report was approved, send the notification" automatically removes the risk of a notification going out before the approval is saved.
There are two kinds of operations:
Controlled (transactionally controlled): IF_BGMC_OP_SINGLE
The recommended kind for RAP applications. The EXECUTE method starts in the modify phase: reading and calculating are allowed, database changes are not. After switching to the save phase with cl_abap_tx=>save( ), persistent changes are made. No COMMIT WORK is written inside EXECUTE; the framework does that. The operation is set with set_operation.
Uncontrolled: IF_BGMC_OP_SINGLE_TX_UNCONTR
For cases outside RAP where transactional control is not needed; there are no additional restrictions. The operation is set with set_operation_tx_uncontrolled. The price of the flexibility is that transactional consistency is entirely up to you.
A controlled operation that creates a notification record when an expense report is approved can be written like this:
CLASS zcl_exp_notify_op DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_bgmc_op_single.
METHODS constructor
IMPORTING report_uuid TYPE sysuuid_x16.
PRIVATE SECTION.
DATA report_uuid TYPE sysuuid_x16.
ENDCLASS.
CLASS zcl_exp_notify_op IMPLEMENTATION.
METHOD constructor.
me->report_uuid = report_uuid.
ENDMETHOD.
METHOD if_bgmc_op_single~execute.
" modify phase: reading and calculating allowed, no database changes
SELECT SINGLE ReportID, CreatedBy
FROM zi_expensereport
WHERE ReportUUID = @report_uuid
INTO @DATA(report).
IF sy-subrc <> 0.
RETURN.
ENDIF.
" switch to the save phase
cl_abap_tx=>save( ).
" save phase: persistent change
INSERT zexp_notif FROM @( VALUE #( report_uuid = report_uuid
recipient = report-CreatedBy
created_at = utclong_current( ) ) ).
ENDMETHOD.
ENDCLASS.The operation object carries only a key. An attribute set in the constructor is available in the background run, but the report's current state is read again inside EXECUTE. The background step runs a little after the user's transaction, not at the same moment, so it is safer not to carry data that may change in between inside the object.
Starting bgPF from RAP
Outside RAP, an explicit COMMIT WORK is needed after the process is registered. Inside RAP, the framework commits, and the process is created during the save sequence. In a managed business object, the place for this is the save_modified method of the saver class enabled with with additional save:
METHOD save_modified.
LOOP AT update-expensereport INTO DATA(report)
WHERE %control-Status = if_abap_behv=>mk-on
AND Status = 'A'.
TRY.
DATA(process) = cl_bgmc_process_factory=>get_default( )->create( ).
process->set_name( |Expense notification { report-ReportID }| ).
process->set_operation( NEW zcl_exp_notify_op( report-ReportUUID ) ).
process->save_for_execution( ).
CATCH cx_bgmc INTO DATA(bgmc_error).
" process could not be registered: write to the application log
ENDTRY.
ENDLOOP.
ENDMETHOD.If the approval is rolled back (a validation error, a lock conflict), RAP does not commit and the notification process never starts. The user, meanwhile, doesn't wait for the notification work to finish after pressing the approve button. For RAP's save sequence and the saver class, see the SAP RAP guide.
Monitoring bgPF
save_for_execution returns a monitor object. Its get_state method returns the process state (such as new, successful or erroneous). The monitor can be serialized to text with to_string, stored, and later restored with cl_bgmc_process_factory=>create_monitor_from_string. That's an easy way to keep a process trail in your own table to answer the user's question "was the notification sent?".
Because bgPF is built on bgRFC, its queues also show up in bgRFC monitoring tools. Monitoring details can differ between cloud and on-premise releases; try the tools in your own system before going live.
Design Recommendations
- Make it rerunnable. A job can be interrupted and restarted. A state-based selection such as "approved reports not yet posted" is safer than a time-based one such as "yesterday's reports": the second run doesn't repeat what the first one did.
- Test mode and logging. Give every mass job a test mode and a readable log. The person reading the log is usually a key user, not a developer.
- Don't load heavy work onto bgPF. bgPF is for the continuation of a transaction; a mass job processing thousands of records belongs in a monitorable application job.
- Keep business logic in one place. If the same change is made from the UI and from the background, both should use the same RAP business object; don't write a second copy of the validations for the background job.
- Measure performance. For a long-running job, first find where the time goes; the tools in the ABAP performance analysis article apply to background jobs as well.
Common Mistakes
Making everything an application job
Setting up a job that runs every five minutes for "send a notification after approval" means both delay and unnecessary load. A step that continues a transaction belongs to bgPF.
Writing in the modify phase of a controlled operation
A database change made before calling cl_abap_tx=>save( ) causes a runtime error. Read and calculate first, then switch the phase, then write.
Letting exceptions other than CX_APJ_RT_CONTENT escape
An unhandled exception leaves a short dump instead of a meaningful message. Catch unexpected errors and convert them into CX_APJ_RT_CONTENT.
Scheduling a job with start_immediately from RAP
The API commits implicitly and breaks RAP's transactional rules. Schedule with a timestamp.
Writing to the table directly in a background job
The UI's validations are skipped in the background and the same object changes under two different rule sets. If there is a RAP business object, use EML.
Forgetting the authorization context
The job reads with the authorizations of the user it runs under. A view with DCL may return all data for the test user and only part of it for the job user in production.
Conclusion
Background processing in ABAP Cloud is not one tool but two answers to two separate problems. Work that is tied to a schedule, parameterized and needs monitoring is an application job: class, catalog entry, template and log. A step that must continue a user transaction in the background, if and only if that transaction succeeds, is bgPF: it is born with the save and dies with the rollback. With that distinction made correctly, users don't wait, and inconsistencies like "the notification went out but the record isn't there" disappear.
You can find the scope I offer for moving your existing SM36 jobs and asynchronous function calls to ABAP Cloud on the process automation services page.
Get support moving your classic background jobs to Application Jobs and bgPF, adding asynchronous steps to your RAP application, or improving error handling in existing jobs.
Request an Initial Consultation