[[{“value”:”
CL_HTTP_CLIENT using SM59 RFC destinations. AI Core uses OAuth 2.0 Client Credentials for authentication.AI_CORE_TOKEN — OAuth 2.0 token endpointAI_CORE_API — Model inference endpointclient_id and client_secret as form-encoded body, get back a JSON response containing access_token.METHOD get_bearer_token.
DATA lo_http TYPE REF TO if_http_client.
cl_http_client=>create_by_destination(
EXPORTING destination = ‘AI_CORE_TOKEN’
IMPORTING client = lo_http
EXCEPTIONS OTHERS = 6 ).
IF sy-subrc <> 0.
RETURN.
ENDIF.
lo_http->request->set_method(
if_http_request=>co_request_method_post ).
lo_http->request->set_header_field(
name = ‘Content-Type’
value = ‘application/x-www-form-urlencoded’ ).
” Client credentials flow
DATA(lv_body) =
|grant_type=client_credentials| &
|&client_id={ c_client_id }| &
|&client_secret={ c_client_secret }| &
|&resource_tenant_id={ c_tenant_id }|.
lo_http->request->set_cdata( lv_body ).
lo_http->send( ).
lo_http->receive( ).
DATA(lv_response) = lo_http->response->get_cdata( ).
lo_http->close( ).
” Extract token from JSON
FIND FIRST OCCURRENCE OF REGEX
‘”access_token”s*:s*”([^”]+)”‘
IN lv_response
SUBMATCHES rv_token.
ENDMETHOD.
system / messages split rather than a single flat prompt array:METHOD call_ai_core.
DATA lo_http TYPE REF TO if_http_client.
cl_http_client=>create_by_destination(
EXPORTING destination = ‘AI_CORE_API’
IMPORTING client = lo_http
EXCEPTIONS OTHERS = 6 ).
lo_http->request->set_method( ‘POST’ ).
lo_http->request->set_header_field(
name = ‘Authorization’
value = |Bearer { iv_token }| ).
lo_http->request->set_header_field(
name = ‘Content-Type’
value = ‘application/json’ ).
lo_http->request->set_header_field(
name = ‘AI-Resource-Group’
value = ‘default’ ).
DATA(lv_body) =
|{| &
|”anthropic_version”:”bedrock-2023-05-31″,| &
|”max_tokens”:1024,| &
|”system”:”{ escape_for_json( build_system_prompt( ) ) }”,| &
|”messages”:[| &
| {“role”:”user”,”content”:”{ escape_for_json( iv_prompt ) }”}| &
|]| &
|}|.
lo_http->request->set_cdata( lv_body ).
lo_http->send( ).
lo_http->receive( ).
rv_result = lo_http->response->get_cdata( ).
lo_http->close( ).
ENDMETHOD.
AI-Resource-Group header is mandatory — AI Core uses it to route to the correct deployment context.You are a filter interpreter for an SAP application.
Convert natural language queries into structured JSON filter parameters.
Rules:
– Only use the exact filter fields and values listed below.
– For coded fields use the exact codes shown.
– Today is [injected at runtime via sy-datum].
– Return ONLY valid JSON. No markdown. No text outside JSON.
=== AVAILABLE FILTER FIELDS ===
Field: “role_type”
Type: CODED
Values:
“SIN” = Single Role
“COM” = Composite Role
“BUS” = Business Role
=== OUTPUT FORMAT ===
{
“filters”: [
{“field”: “role_type”, “operator”: “EQ”, “value”: “SIN”, “label”: “Role Type = Single”}
],
“interpretation”: “Plain English summary of what was understood”,
“confidence”: 90,
“warnings”: “Any assumptions, or empty string”
}
{
“content”: [
{“type”: “text”, “text”: “{…your structured JSON…}”}
]
}
/UI2/CL_JSON:METHOD extract_json_from_response.
FIND FIRST OCCURRENCE OF REGEX
‘”text”s*:s*”((?:[^”\]|\.)*)”‘
IN iv_raw_response
SUBMATCHES rv_json.
REPLACE ALL OCCURRENCES OF ‘”‘ IN rv_json WITH ‘”‘.
REPLACE ALL OCCURRENCES OF ‘n’ IN rv_json WITH ‘ ‘.
REPLACE ALL OCCURRENCES OF ‘\’ IN rv_json WITH ”.
ENDMETHOD.
/ui2/cl_json=>deserialize(
EXPORTING json = lv_json
pretty_name = /ui2/cl_json=>pretty_mode-camel_case
CHANGING data = ls_result ).
/UI2/CL_JSON handles camelCase to snake_case mapping automatically when you use pretty_mode-camel_case. No manual field mapping needed.DATA(ls_result) = zcl_nl_filter_poc=>parse_nl_to_filter(
iv_query = ‘Show me high sensitivity composite roles in the production release’ ).
IF ls_result-success = abap_true.
” ls_result-filter_json -> ready to apply as filter conditions
” ls_result-interpretation -> show user what was understood
” ls_result-confidence -> 0-100, warn if below threshold
” ls_result-warnings -> surface assumptions to user
ENDIF.
parse_nl_to_filter, and writes the result back to the entity.invokeAction on the binding context, passes the search field value, and OData V4 side effects take care of refreshing the UI automatically once the action returns. No manual model refresh needed.
“}]]
[[{“value”:”The Itch I Had to ScratchI was working on a module where users needed to search through hundreds of business roles using a classic SAP filter panel — dropdowns, input fields, coded values. Fine. But I kept thinking: what if someone could just type what they want in plain English? “Show me high-sensitivity finance roles in production.” That sentence maps to at least four filter fields, each with a coded value the user has no reason to know. The UI couldn’t do it. The roadmap wasn’t going to do it soon either. So I decided to find out whether I could wire SAP AI Core — specifically Claude — directly into ABAP, in an afternoon, as a proof of concept. The answer was yes. Here’s how it works, and more importantly, why the pattern is useful far beyond my specific use case. SAP AI Core exposes a REST API. ABAP can make HTTP calls via CL_HTTP_CLIENT using SM59 RFC destinations. AI Core uses OAuth 2.0 Client Credentials for authentication. So, the flow is: ABAP → OAuth token endpoint (SM59 destination) → get bearer token → ABAP → AI Core inference endpoint (second SM59 destination) → send prompt → parse JSON response back into ABAP structures. That’s it. No middleware, no BTP sidecar, no external service. Pure ABAP. Step 1: Two SM59 Destinations Everything starts in SM59 (Transaction for RFC/HTTP destination configuration). You need two HTTP destinations: AI_CORE_TOKEN — OAuth 2.0 token endpointAI_CORE_API — Model inference endpoint The token destination needs no authentication (you’ll pass credentials in the body). The API destination uses the bearer token you fetch at runtime. Both use HTTPS — make sure your SAP system has the correct SSL certificates imported via STRUST. Note: The deployment ID in the API path is specific to your AI Core deployment. In my case I was using Claude via Bedrock through AI Core’s proxy. Step 2: Fetching the OAuth Token in ABAP The OAuth 2.0 Client Credentials flow is straightforward: POST your client_id and client_secret as form-encoded body, get back a JSON response containing access_token. METHOD get_bearer_token.
DATA lo_http TYPE REF TO if_http_client.
cl_http_client=>create_by_destination(
EXPORTING destination = ‘AI_CORE_TOKEN’
IMPORTING client = lo_http
EXCEPTIONS OTHERS = 6 ).
IF sy-subrc <> 0.
RETURN.
ENDIF.
lo_http->request->set_method(
if_http_request=>co_request_method_post ).
lo_http->request->set_header_field(
name = ‘Content-Type’
value = ‘application/x-www-form-urlencoded’ ).
” Client credentials flow
DATA(lv_body) =
|grant_type=client_credentials| &
|&client_id={ c_client_id }| &
|&client_secret={ c_client_secret }| &
|&resource_tenant_id={ c_tenant_id }|.
lo_http->request->set_cdata( lv_body ).
lo_http->send( ).
lo_http->receive( ).
DATA(lv_response) = lo_http->response->get_cdata( ).
lo_http->close( ).
” Extract token from JSON
FIND FIRST OCCURRENCE OF REGEX
‘”access_token”s*:s*”([^”]+)”‘
IN lv_response
SUBMATCHES rv_token.
ENDMETHOD. Step 3: Calling the Model With a token in hand, calling the inference endpoint is a standard HTTP POST. The important detail for Claude is the request body format — Claude uses a system / messages split rather than a single flat prompt array:METHOD call_ai_core.
DATA lo_http TYPE REF TO if_http_client.
cl_http_client=>create_by_destination(
EXPORTING destination = ‘AI_CORE_API’
IMPORTING client = lo_http
EXCEPTIONS OTHERS = 6 ).
lo_http->request->set_method( ‘POST’ ).
lo_http->request->set_header_field(
name = ‘Authorization’
value = |Bearer { iv_token }| ).
lo_http->request->set_header_field(
name = ‘Content-Type’
value = ‘application/json’ ).
lo_http->request->set_header_field(
name = ‘AI-Resource-Group’
value = ‘default’ ).
DATA(lv_body) =
|{| &
|”anthropic_version”:”bedrock-2023-05-31″,| &
|”max_tokens”:1024,| &
|”system”:”{ escape_for_json( build_system_prompt( ) ) }”,| &
|”messages”:[| &
| {“role”:”user”,”content”:”{ escape_for_json( iv_prompt ) }”}| &
|]| &
|}|.
lo_http->request->set_cdata( lv_body ).
lo_http->send( ).
lo_http->receive( ).
rv_result = lo_http->response->get_cdata( ).
lo_http->close( ).
ENDMETHOD. The AI-Resource-Group header is mandatory — AI Core uses it to route to the correct deployment context. Step 4: The Prompt Engineering Part (Where the Real Work Is) This is where I spent most of my time, and where I think the most interesting learning sits. The goal: Given a free-text user query, return a predictable, structured JSON object that my ABAP code can deserialize directly into a typed structure. The technique is called constrained structured output via prompt engineering. You tell the model exactly what schema to follow, exactly what coded values are valid, and you instruct it to return only valid JSON — no markdown, no preamble. A simplified version of my system prompt: You are a filter interpreter for an SAP application.
Convert natural language queries into structured JSON filter parameters.
Rules:
– Only use the exact filter fields and values listed below.
– For coded fields use the exact codes shown.
– Today is [injected at runtime via sy-datum].
– Return ONLY valid JSON. No markdown. No text outside JSON.
=== AVAILABLE FILTER FIELDS ===
Field: “role_type”
Type: CODED
Values:
“SIN” = Single Role
“COM” = Composite Role
“BUS” = Business Role
=== OUTPUT FORMAT ===
{
“filters”: [
{“field”: “role_type”, “operator”: “EQ”, “value”: “SIN”, “label”: “Role Type = Single”}
],
“interpretation”: “Plain English summary of what was understood”,
“confidence”: 90,
“warnings”: “Any assumptions, or empty string”
} Step 5: Parsing the Response Back Into ABAP The Claude response wraps the model output in a JSON envelope:{
“content”: [
{“type”: “text”, “text”: “{…your structured JSON…}”}
]
} I extract the inner JSON with a regex, unescape it, then deserialize it with /UI2/CL_JSON:METHOD extract_json_from_response.
FIND FIRST OCCURRENCE OF REGEX
‘”text”s*:s*”((?:[^”\]|\.)*)”‘
IN iv_raw_response
SUBMATCHES rv_json.
REPLACE ALL OCCURRENCES OF ‘”‘ IN rv_json WITH ‘”‘.
REPLACE ALL OCCURRENCES OF ‘n’ IN rv_json WITH ‘ ‘.
REPLACE ALL OCCURRENCES OF ‘\’ IN rv_json WITH ”.
ENDMETHOD. Then the deserialization:/ui2/cl_json=>deserialize(
EXPORTING json = lv_json
pretty_name = /ui2/cl_json=>pretty_mode-camel_case
CHANGING data = ls_result ). /UI2/CL_JSON handles camelCase to snake_case mapping automatically when you use pretty_mode-camel_case. No manual field mapping needed. The ResultA single public method call:DATA(ls_result) = zcl_nl_filter_poc=>parse_nl_to_filter(
iv_query = ‘Show me high sensitivity composite roles in the production release’ ).
IF ls_result-success = abap_true.
” ls_result-filter_json -> ready to apply as filter conditions
” ls_result-interpretation -> show user what was understood
” ls_result-confidence -> 0-100, warn if below threshold
” ls_result-warnings -> surface assumptions to user
ENDIF. Wiring It Into a Fiori AppThe class works as a standalone utility, but the real payoff comes when you surface it in a real UI. I exposed it through a RAP custom action on an OData V4 service — the action receives the natural language query as a parameter, calls parse_nl_to_filter, and writes the result back to the entity. On the Fiori side, the controller calls invokeAction on the binding context, passes the search field value, and OData V4 side effects take care of refreshing the UI automatically once the action returns. No manual model refresh needed. And this is how it looked like!! Final ThoughtThe thing that surprised me most was how little plumbing was needed. Two SM59 destinations, one HTTP client call for auth, one for inference, a regex to unpack the response. The bulk of the work was the prompt itself — which is a different kind of engineering than I’m used to, but a genuinely interesting one. If you’re an ABAP developer wondering whether AI is “for you” or only for the frontend teams and architects with BTP budgets — it’s for you too. The tools are already there.”}]] Read More Technology Blog Posts by SAP articles
#SAPCHANNEL