Building Custom Joule Capabilities: Private Document Grounding with SAP AI Core

Estimated read time 39 min read

This is part of a series on building custom SAP Joule capabilities. Start with Building Custom Joule Capabilities: Getting Started if you haven’t set up your environment yet.Blog Post 0: Getting started : Get started with Joule Capability Development using Joule Studio CLIBlog Post 1: Joule Message Types: Overview of Joule message types with practical examples and recommendationsBlog Post 2: Document Grounding: Connecting Joule to document sources so capabilities can retrieve and cite content from your organization’s files.Blog Post 3: Private Document Grounding with SAP AI Core (This): Isolating grounding data to specific capabilities using grounding in SAP AI Core.Blog Post 4: Role-Based Access Control: Restricting which users can invoke which scenarios and actions based on their IAS group membership and other IAS user attributes. 

In Post 2, we built a Product Compliance capability that combines live Northwind OData with answers retrieved from Google Drive documents using the shared document grounding service in Joule. At the end of that post we noted an important caveat about this:

The shared grounding service has no concept of per-capability data isolation. Any pipeline indexed there is reachable through Joule regardless of which capability you built on top of it.

That caveat matters most when the documents are genuinely sensitive. HR policy documents about remote work or office locations are low risk. Salary bands, bonus structures, and equity guidelines are a different story. If those documents end up in the shared grounding index, any employee can ask Joule about them and get answers.

This post solves that. By moving the grounding infrastructure into your own SAP AI Core instance, documents never enter the shared index. The only path to them is through a capability you control. That gives you the data-isolation layer of a defence-in-depth model. A second, complementary layer that gates the scenario itself by user attributes via visibility_condition is covered in Post 4: Role-Based Scenario Access. We’ll show what that addition looks like at the end of the scenario step so you can see how the two layers fit together, but the deployed capability in this post focuses on the data-isolation layer.

This blog post builds an HR Compensation Search capability that lets users query salary bands, bonus structures, and equity guidelines. The documents are indexed in a private AI Core grounding pipeline and only exposed when the scenario of the capability is triggered.

 

Note for SAP SuccessFactors customers: If your goal is specifically HR policy documents in SuccessFactors Joule, there is a dedicated Manage Document Grounding admin app in AI Services Administration that handles connector setup, metadata, and user attribute mapping through a UI. This post targets sensitive documents in the general case and uses an HR use case for illustration purposes, where you own the AI Core instance and need full control over isolation

Why Private Grounding?

Before building, it is worth understanding exactly what changes when you move from shared to private grounding.

With the shared service, your pipeline and its documents live in Joule’s shared Document Grounding instance. Joule automatically searches all pipelines in that instance. Any user who asks Joule a question that semantically matches your documents will get an answer. Even if you gate your own custom capability with a visibility_condition, that gate only stops users from triggering your specific flow. It cannot stop Joule from surfacing the same content through its built-in grounding.

With AI Core and its orchestration workflow, your grounding pipeline lives in your own AI Core instance. By default, Joule has no access to it. The only way to reach the documents is through a system_alias that points at the AI Core instance, and that alias only exists in your custom capability. A user who bypasses this capability entirely gets nothing.

Here is the comparison at a glance:

AspectShared Grounding (Post 2)Private Grounding (Post 3)System aliasJOULE_GLOBAL_DOC_GROUNDING (reserved)Custom alias, your BTP destinationAuthenticationHandled by Joule runtimeOAuth2ClientCredentials via AI Core service keyDocument visibilityAll Joule usersOnly reachable through your capabilityPipeline managementJoule Document Grounding service AI Core Pipelines APIExtra header requiredNoneAI-Resource-Group

The rule of thumb is straightforward. Use shared grounding for company-wide knowledge bases and general content. Use private grounding for confidential content where data isolation is a requirement.

Architecture Overview

Here is how the full flow works with private grounding:

An HR Manager asks Joule: “What are the salary bands for senior engineers?”Joule routes the query to the search_compensation dialog function.The function fires a POST request to the AI Core retrieval endpoint via the newly created AICORE_PRIVATE_GROUNDING BTP destination.The BTP destination service fetches a Bearer token from AI Core’s OAuth endpoint and attaches it to the request.AI Core searches the private pipeline for semantically similar chunks.The top-ranked chunks come back in the response.Joule’s LLM synthesizes an answer from those chunks and presents it to the user.

The key differences from the shared grounding service are steps 3 and 4. Instead of the Joule runtime routing through a reserved alias, a real BTP destination handles OAuth token acquisition. You configure this once in the BTP Cockpit. Your capability YAML never touches a credential.

Prerequisites

Before you begin, make sure you have:

Joule Studio CLI installed and authenticated (see Post 0)SAP AI Core instance with the generative AI hub enabled in your BTP subaccountAI Core service key with access to the document grounding APIsGoogle Drive folder with compensation documents, shared with a GCP service account (see Setting Up Document Grounding with Google Drive on SAP BTP for the GCP service account setup)

Step 1: Set Up AI Core for Grounding

If you already use SAP AI Core for LLM orchestration, the document grounding APIs are available in the same instance. No separate service is required. If you do not have an AI Core instance, provision one in your BTP subaccount by finding “SAP AI Core” in the Service Marketplace and creating an instance with the extended plan (required for generative AI hub access).

Once your instance is ready, create a service binding. In the BTP Cockpit, navigate to your AI Core instance and open the Service Bindings tab. Click Create, give the binding a name, and confirm.

Once the binding is created, click View Credentials to open the JSON and note these four values:

Depending on your runtime environment, this may be a service key rather than a service binding. On Cloud Foundry, AI Core credentials are issued as a service key. On Kyma or other Kubernetes runtimes, they appear as a service binding. The JSON fields and values are identical in both cases, so the rest of this setup is unchanged.JSON FieldPurposeclientidOAuth2 client ID for the BTP destinationclientsecretOAuth2 client secret for the BTP destinationurlOAuth2 token endpoint base URL (append /oauth/token)serviceurls.AI_API_URLBase URL for all AI Core API calls

Before making any AI Core API calls, fetch a Bearer token using the credentials from your service binding. Replace <url>, <clientid>, and <clientsecret> with the corresponding values from your binding credentials:

curl –request POST “<url>/oauth/token”
–header “Content-Type: application/x-www-form-urlencoded”
–data “grant_type=client_credentials”
–data “client_id=<clientid>”
–data “client_secret=<clientsecret>”

The response contains an access_token field. Copy that value and save it. You will need it for the next request and throughout the rest of this setup.

Create a resource group for grounding. AI Core organizes resources by resource group, and the grounding APIs require an AI-Resource-Group header on every request. A resource group used for document grounding must carry a specific label (ext.ai.sap.com/document-grounding: true). Without this label, the grounding service will not recognize the group. Create it with a POST request:

curl –request POST “$AI_API_URL/v2/admin/resourceGroups”
–header “Authorization: Bearer $ACCESS_TOKEN”
–header “Content-Type: application/json”
–data ‘{
“resourceGroupId”: “grounding”,
“labels”: [
{
“key”: “ext.ai.sap.com/document-grounding”,
“value”: “true”
}
]
}’

You can choose any valid ID for resourceGroupId (3 to 253 characters, letters, numbers, . and – allowed, must start and end with a letter or number). Substitute it wherever this post shows grounding. If you have an existing resource group you want to reuse, send a PATCH request to the same endpoint with just the labels array to add the grounding label.

Step 2: Create the BTP Destination

This destination replaces the reserved shared grounding JOULE_GLOBAL_DOC_GROUNDING alias used in Post 2. The BTP destination service handles the OAuth token lifecycle. Your capability YAML simply references the destination name.

Open Connectivity > Destinations in your BTP Cockpit and create a new destination with these properties:

PropertyValueNameAICORE_PRIVATE_GROUNDINGTypeHTTPURL<AI_API_URL> from the service binding credentialsProxy TypeInternetAuthenticationOAuth2ClientCredentialsClient ID<clientid> from the service binding credentialsClient Secret<clientsecret> from the service binding credentialsToken Service URL<url>/oauth/token from the service binding credentials

Save the destination and click Check Connection. A successful response confirms BTP can reach the AI Core API with valid credentials.

This is the same pattern used for any OAuth2-protected SAP service. The destination abstracts authentication entirely. When your Joule capability sends a request through this destination, BTP automatically attaches a valid Bearer token.

Step 3: Configure the Google Drive Connector in AI Core

The following section covers the setup for document grounding with Google Drive. However, the same can be done via other allowed knowledge sourcs, such as Microsoft SharePoint.

AI Core’s document grounding service uses generic secrets to hold connector credentials. Think of a generic secret as the AI Core equivalent of a BTP destination. It stores the connection details for a data source.

Create a generic secret holding your GCP service account key. The endpoint is /v2/admin/secrets and all credential values must be base64-encoded. You can base64-encode a value on the command line with echo -n “<value>” | base64. 

curl –request POST
–url “<AI_API_URL>/v2/admin/secrets”
–header “Authorization: Bearer <access_token>”
–header “AI-Resource-Group: grounding”
–header “Content-Type: application/json”
–data-raw ‘{
“name”: “gdrive-compensation”,
“data”: {
“clientId”: “<BASE64_ENCODED_CLIENT_ID>”,
“private_key”: “<BASE64_ENCODED_PRIVATE_KEY>”,
“client_email”: “<BASE64_ENCODED_CLIENT_EMAIL>”,
“auth_uri”: “<BASE64_ENCODED_AUTH_URI>”,
“authentication”: “T0F1dGgyQ2xpZW50Q3JlZGVudGlhbHM=”,
“url”: “aHR0cHM6Ly9nb29nbGVhcGlzLmNvbS9kcml2ZS92Mw==”,
“tokenServiceURL”: “aHR0cHM6Ly9vYXV0aDIuZ29vZ2xlYXBpcy5jb20vdG9rZW4=”
},
“labels”: [
{
“key”: “ext.ai.sap.com/document-grounding”,
“value”: “true”
},
{
“key”: “ext.ai.sap.com/documentRepositoryType”,
“value”: “GoogleDrive”
}
]
}’

The authentication, url, and tokenServiceURL values above are fixed base64-encoded constants. Do not change them. Only clientId, private_key, client_email, and auth_uri come from your GCP service account key JSON.

The name field (gdrive-compensation) is what you reference in the pipeline creation request. This is distinct from the BTP destination name.

Once the generic secret is created, create the pipeline pointing at your compensation documents folder:

curl
–request POST
–url “<AI_API_URL>/v2/lm/document-grounding/pipelines”
–header “Authorization: Bearer <access_token>”
–header “AI-Resource-Group: grounding”
–header “content-type: application/json”
–data ‘{
“type”: “GoogleDrive”,
“configuration”: {
“destination”: “gdrive-compensation”,
“googleDrive”: {
“resourceType”: “SHARED_FOLDER”,
“resourceId”: “<your-compensation-folder-id>”
}
}
}’

Note that configuration.destination refers to the generic secret name (gdrive-compensation), not the BTP destination name. The naming overlap is unfortunate but they are separate systems.

If you want to index only specific subfolders rather than the entire shared folder, add an includePaths array inside googleDrive. For example, “includePaths”: [“CompensationDocs/2025”] would limit ingestion to that subfolder only.

“googleDrive”: {
“resourceType”: “SHARED_FOLDER”,
“resourceId”: “<your-compensation-folder-id>”,
“includePaths”: [
“CompensationDocs/2025”
]
}

The response includes a pipelineId. Save it. Poll the pipeline status until it returns FINISHED:

curl
–request GET
–url “<AI_API_URL>/v2/lm/document-grounding/pipelines/<pipelineId>”
–header “Authorization: Bearer <access_token>”
–header “AI-Resource-Group: grounding”

Ingestion for a small document set typically completes within a few minutes. For this example, upload the three sample compensation documents from the joule-dev-blog-samples GitHub repository under Blog_03_private_grounding/sample-documents/ to your Google Drive folder.

Step 4: Create the Capability

Create this folder structure inside your capabilities/ directory:

capabilities/
private_grounding/
capability.sapdas.yaml
scenarios/
search_compensation.yaml
functions/
search_compensation.yaml

Start with capability.sapdas.yaml:

schema_version: 3.28.0

metadata:
namespace: joule.ext
name: hr_compensation_search_capability
version: 1.0.0
display_name: HR Compensation Search
description: Search confidential HR compensation policies grounded in private AI Core documents

system_aliases:
AICORE_PRIVATE_GROUNDING:
destination: AICORE_PRIVATE_GROUNDING

Unlike Post 2’s JOULE_GLOBAL_DOC_GROUNDING, the destination name here (AICORE_PRIVATE_GROUNDING) is a real BTP destination you created in Step 2. The Joule runtime will look up this destination by name, retrieve credentials, and use them when making the API call. You own this destination, so you can update it, move it, or rotate credentials without touching the capability YAML.

Step 5: The Scenario

Create scenarios/search_compensation.yaml:

description: >
Search compensation policies, salary bands, bonus structures, and equity guidelines.
Retrieve grounded answers from confidential HR compensation documents.

slots:
– name: query
description: The compensation policy question to search for
target:
name: search_compensation
type: function

response_context:
– value: $target_result.jouleResponse
description: Joule answer to grounding query

The description covers the full range of compensation topics your documents address. Including concrete terms (“salary bands”, “bonus structures”, “equity guidelines”) improves Joule’s intent matching for this scenario. The response_context exposes the jouleResponse variable produced by the dialog function (defined in the next step) to Joule’s LLM for response generation.

Optional: Restricting the scenario by user group

The capability above already isolates the data. Only this capability can reach the AI Core pipeline, and Joule information retrieval cannot. That is the data-isolation layer. You can add a second, complementary layer that controls which users are allowed to trigger the scenario in the first place. Joule supports this declaratively through a visibility_condition block on the scenario:

visibility_condition:
objects:
– type: ias_attributes
attributes:
– attribute: groups
match: ANY
values:
– HR_MANAGERS

Adding this block to the scenario above gates it to users in the IAS group HR_MANAGERS. A user outside that group cannot trigger this scenario at all, even though they would otherwise be able to. Combined with the private pipeline, this gives you a defence-in-depth model. The data is reachable only through this capability, and this capability is reachable only by authorized users.

visibility_condition has its own dedicated post in this series that walks through the full attribute model (e.g. groups or email), the ANY vs ALL match modes, IAS group setup, and how to combine multiple condition objects. We will not configure it on the deployed capability in this post so that you can test the data-isolation layer in isolation.

Step 6: The Dialog Function

Create functions/search_compensation.yaml:

parameters:
– name: query
optional: false
action_groups:
– actions:
– type: api-request
method: POST
headers:
content-type: application/json
AI-Resource-Group: grounding
system_alias: AICORE_PRIVATE_GROUNDING
path: “/v2/lm/document-grounding/retrieval/search”
body: >
{
“query”: “<? query ?>”,
“filters”: [
{
“id”: “compensation-filter”,
“searchConfiguration”: {
“maxChunkCount”: 3
},
“dataRepositories”: [“*”],
“dataRepositoryType”: “vector”,
“documentMetadata”: [],
“chunkMetadata”: []
}
]
}
result_variable: search_results
– type: set-variables
variables:
– name: jouleResponse
value: “The result for the given userQuery – <? query ?> is: <? search_results.body ?>”

result:
jouleResponse: <? jouleResponse ?>

Comparing this to Post 2’s function, three things changed:

system_alias: AICORE_PRIVATE_GROUNDING replaces JOULE_GLOBAL_DOC_GROUNDING. This tells the runtime to use your BTP destination instead of the reserved alias.

path: “/v2/lm/document-grounding/retrieval/search” is the full AI Core API path. With the shared grounding service, the path was /retrieval/api/v1/search because the alias already pointed at the grounding service base URL. Here the destination URL is the AI Core root, so the full path is required.

AI-Resource-Group: grounding is a header that AI Core requires on every document grounding API call. It scopes the request to the resource group you created in Step 1. If you used a different ID for your resource group, substitute it here.

The retrieval body, maxChunkCount: 3, and the jouleResponse pattern follow the same logic as Post 2. Fetch the top three chunks and let Joule’s LLM pick the most relevant content for the user’s question. The AI Core retrieval endpoint returns the same response structure as the shared grounding service.

Step 7: Deploy and Test

Validate and deploy:

joule lint
joule deploy -c -n “hr_compensation_test”
joule launch “hr_compensation_test”

Test the capability inside the hr_compensation_test test instance you launched above. Try these queries:

“What are the salary bands for senior engineers?””What is the bonus structure for IC3 level?””How does equity vesting work at ACME?””What are the RSU grant ranges for principal level?”

Now verify the isolation. Switch from the hr_compensation_test window to the central sap_digital_assistant Joule and ask the same question directly without going through your capability: “What are the salary bands at ACME?” You should get a response indicating no relevant documents were found. The compensation documents are not in the shared index, so central Joule cannot surface them. The capability is the only path to the data.

If the compensation search capability returns no results, check the resource group header. The AI-Resource-Group value in the function must match the resource group where you created the pipeline.

Comparing the Two Approaches

You now have two document grounding capabilities in your Digital Assistant:

Product Compliance (Post 2) Northwind product data combined with compliance guidelines from the shared grounding service, accessible to all employees through your capability and through Joule.HR Compensation Search (this post): Confidential compensation data in a private AI Core pipeline, reachable only through this capability. Add a visibility_condition (covered in Post 4) to additionally restrict who can trigger the scenario.

Both capabilities coexist in the same Digital Assistant without conflict. The shared grounding alias and the AI Core destination are independent. Each capability handles its own retrieval path.

The decision between shared and private grounding comes down to one question. Should the information/document be available to all Joule users? If yes, shared grounding is simpler to set up and requires no BTP destination. If no, private grounding is the only option that provides actual data isolation.

A few additional guidelines:

Shared grounding works well for anything you would put on an internal wiki. Examples include office information, general HR policies, IT guides, and onboarding materials.Private grounding is appropriate for any content where unauthorized access would be a compliance, legal, or business problem. Examples include compensation data, financial projections, M&A documents, legal analysis, and board materials.You can run multiple private pipelines in the same AI Core instance and serve them from multiple capabilities. Each capability can independently add a visibility_condition once you have read Post 4: Role-Based Scenario Access.If your data repository is Microsoft SharePoint, a third security option exists: per-document access control that mirrors SharePoint ACLs into the grounding index, so two users hitting the same scenario each see only the documents they are personally permitted to view in SharePoint. See “What This Post Does Not Cover” below.

What This Post Does Not Cover

The capability in this post uses the minimum surface of the AI Core grounding module needed to make the data-isolation point: one pipeline, one filter, three chunks, and Joule response generation. The same setup sits on top of a much larger grounding API that this post does not exercise. For a real-world HR or compliance corpus you will likely want some of the following.

Per-document metadata via a Metadata API Server. AI Core can call an OAuth2-protected metadata endpoint during pipeline ingestion to attach custom keys to each document, for example department, region, level, or domainId. Retrieval can then be scoped to documents for region=EMEA and level=IC5 rather than everything in the pipeline. See Prepare your Metadata API Server.Multi-level metadata filtering and complex match expressions. The /retrieval/search payload accepts filters at the data-repository, document, and chunk levels, with ANY and ALL match modes and nested AND/OR expressions. This blog post sets all three filter arrays to empty. See Vector Search.Post-processing and reranking. The retrieval endpoint accepts a postProcessing block that can merge results across multiple filters or reorder them using the Cohere 3.5 reranker, including metadata-based boosting. For three sample documents, raw vector similarity is enough. For a larger compensation corpus that mixes salary bands, bonus structures, and equity policies, reranking and boosting on the user’s level or region make a measurable difference to result quality. See Retrieval Search.Other ingestion paths. The same grounding module supports Microsoft SharePoint, AWS S3, SFTP, SAP Build Work Zone, SAP Document Management service, and ServiceNow alongside Google Drive. Per-document access control (Microsoft SharePoint only). When the data repository is Microsoft SharePoint, you can set accessControlEnabled: true on the pipeline. The grounding service then mirrors SharePoint’s per-document permissions into the index, including Entra ID security groups, direct user assignments, and inherited folder permissions. At query time, document grounding checks the signed-in user’s identity against those permissions and returns only the documents that user is allowed to see in SharePoint. This gives you a third, per-document layer that sits on top of the data-isolation layer in this post and the scenario-level visibility_condition in Post 4. It also requires admin consent for the Group.Read.All and User.Read.All Microsoft Graph permissions, the user’s IAS email must match their Entra ID email, and the setting is one-way (it cannot be turned off once enabled). At the time of writing it is available only on AWS eu10 and Google Cloud eu30. See Enable Document Access Control for Microsoft SharePoint.SAP AI Launchpad. Pipelines, generic secrets, collections, documents, and executions in your AI Core resource group can be inspected and managed in the SAP AI Launchpad UI. The curl flow in this post is the developer setup path. Once the capability is wired, the people maintaining the document corpus do not need to touch curl.

None of these require changes to the capability YAML. Only the retrieval payload and the data and metadata behind it get richer.

Conclusion

Building Custom Joule Capabilities: Document Grounding  established document grounding access to the shared grounding instance . This post covered an option to use document grounding only in specific capabilities . By moving your pipeline into AI Core and backing the system alias with a real BTP destination, you control who can reach the documents.

The capability code barely changes. The real work is infrastructure. You need an AI Core service key, a BTP destination, a generic secret in AI Core, and a pipeline pointing at a Google Drive folder. Once those are in place, the data is reachable only through this capability. Layering a visibility_condition on top of that, as covered in Post 4 – Building Custom Joule Capabilities: Role-Based Scenario Access in Joule, gives you the genuine two-layer model, with data isolation at the storage layer and flow control at the scenario layer.

In the next post, we look at gating the scenario itself by user attributes using visibility_condition and IAS group membership. 

 

​ This is part of a series on building custom SAP Joule capabilities. Start with Building Custom Joule Capabilities: Getting Started if you haven’t set up your environment yet.Blog Post 0: Getting started : Get started with Joule Capability Development using Joule Studio CLIBlog Post 1: Joule Message Types: Overview of Joule message types with practical examples and recommendationsBlog Post 2: Document Grounding: Connecting Joule to document sources so capabilities can retrieve and cite content from your organization’s files.Blog Post 3: Private Document Grounding with SAP AI Core (This): Isolating grounding data to specific capabilities using grounding in SAP AI Core.Blog Post 4: Role-Based Access Control: Restricting which users can invoke which scenarios and actions based on their IAS group membership and other IAS user attributes. In Post 2, we built a Product Compliance capability that combines live Northwind OData with answers retrieved from Google Drive documents using the shared document grounding service in Joule. At the end of that post we noted an important caveat about this:The shared grounding service has no concept of per-capability data isolation. Any pipeline indexed there is reachable through Joule regardless of which capability you built on top of it.That caveat matters most when the documents are genuinely sensitive. HR policy documents about remote work or office locations are low risk. Salary bands, bonus structures, and equity guidelines are a different story. If those documents end up in the shared grounding index, any employee can ask Joule about them and get answers.This post solves that. By moving the grounding infrastructure into your own SAP AI Core instance, documents never enter the shared index. The only path to them is through a capability you control. That gives you the data-isolation layer of a defence-in-depth model. A second, complementary layer that gates the scenario itself by user attributes via visibility_condition is covered in Post 4: Role-Based Scenario Access. We’ll show what that addition looks like at the end of the scenario step so you can see how the two layers fit together, but the deployed capability in this post focuses on the data-isolation layer.This blog post builds an HR Compensation Search capability that lets users query salary bands, bonus structures, and equity guidelines. The documents are indexed in a private AI Core grounding pipeline and only exposed when the scenario of the capability is triggered. Note for SAP SuccessFactors customers: If your goal is specifically HR policy documents in SuccessFactors Joule, there is a dedicated Manage Document Grounding admin app in AI Services Administration that handles connector setup, metadata, and user attribute mapping through a UI. This post targets sensitive documents in the general case and uses an HR use case for illustration purposes, where you own the AI Core instance and need full control over isolationWhy Private Grounding?Before building, it is worth understanding exactly what changes when you move from shared to private grounding.With the shared service, your pipeline and its documents live in Joule’s shared Document Grounding instance. Joule automatically searches all pipelines in that instance. Any user who asks Joule a question that semantically matches your documents will get an answer. Even if you gate your own custom capability with a visibility_condition, that gate only stops users from triggering your specific flow. It cannot stop Joule from surfacing the same content through its built-in grounding.With AI Core and its orchestration workflow, your grounding pipeline lives in your own AI Core instance. By default, Joule has no access to it. The only way to reach the documents is through a system_alias that points at the AI Core instance, and that alias only exists in your custom capability. A user who bypasses this capability entirely gets nothing.Here is the comparison at a glance:AspectShared Grounding (Post 2)Private Grounding (Post 3)System aliasJOULE_GLOBAL_DOC_GROUNDING (reserved)Custom alias, your BTP destinationAuthenticationHandled by Joule runtimeOAuth2ClientCredentials via AI Core service keyDocument visibilityAll Joule usersOnly reachable through your capabilityPipeline managementJoule Document Grounding service AI Core Pipelines APIExtra header requiredNoneAI-Resource-GroupThe rule of thumb is straightforward. Use shared grounding for company-wide knowledge bases and general content. Use private grounding for confidential content where data isolation is a requirement.Architecture OverviewHere is how the full flow works with private grounding:An HR Manager asks Joule: “What are the salary bands for senior engineers?”Joule routes the query to the search_compensation dialog function.The function fires a POST request to the AI Core retrieval endpoint via the newly created AICORE_PRIVATE_GROUNDING BTP destination.The BTP destination service fetches a Bearer token from AI Core’s OAuth endpoint and attaches it to the request.AI Core searches the private pipeline for semantically similar chunks.The top-ranked chunks come back in the response.Joule’s LLM synthesizes an answer from those chunks and presents it to the user.The key differences from the shared grounding service are steps 3 and 4. Instead of the Joule runtime routing through a reserved alias, a real BTP destination handles OAuth token acquisition. You configure this once in the BTP Cockpit. Your capability YAML never touches a credential.PrerequisitesBefore you begin, make sure you have:Joule Studio CLI installed and authenticated (see Post 0)SAP AI Core instance with the generative AI hub enabled in your BTP subaccountAI Core service key with access to the document grounding APIsGoogle Drive folder with compensation documents, shared with a GCP service account (see Setting Up Document Grounding with Google Drive on SAP BTP for the GCP service account setup)Step 1: Set Up AI Core for GroundingIf you already use SAP AI Core for LLM orchestration, the document grounding APIs are available in the same instance. No separate service is required. If you do not have an AI Core instance, provision one in your BTP subaccount by finding “SAP AI Core” in the Service Marketplace and creating an instance with the extended plan (required for generative AI hub access).Once your instance is ready, create a service binding. In the BTP Cockpit, navigate to your AI Core instance and open the Service Bindings tab. Click Create, give the binding a name, and confirm.Once the binding is created, click View Credentials to open the JSON and note these four values:Depending on your runtime environment, this may be a service key rather than a service binding. On Cloud Foundry, AI Core credentials are issued as a service key. On Kyma or other Kubernetes runtimes, they appear as a service binding. The JSON fields and values are identical in both cases, so the rest of this setup is unchanged.JSON FieldPurposeclientidOAuth2 client ID for the BTP destinationclientsecretOAuth2 client secret for the BTP destinationurlOAuth2 token endpoint base URL (append /oauth/token)serviceurls.AI_API_URLBase URL for all AI Core API callsBefore making any AI Core API calls, fetch a Bearer token using the credentials from your service binding. Replace <url>, <clientid>, and <clientsecret> with the corresponding values from your binding credentials:curl –request POST “<url>/oauth/token”
–header “Content-Type: application/x-www-form-urlencoded”
–data “grant_type=client_credentials”
–data “client_id=<clientid>”
–data “client_secret=<clientsecret>”The response contains an access_token field. Copy that value and save it. You will need it for the next request and throughout the rest of this setup.Create a resource group for grounding. AI Core organizes resources by resource group, and the grounding APIs require an AI-Resource-Group header on every request. A resource group used for document grounding must carry a specific label (ext.ai.sap.com/document-grounding: true). Without this label, the grounding service will not recognize the group. Create it with a POST request:curl –request POST “$AI_API_URL/v2/admin/resourceGroups”
–header “Authorization: Bearer $ACCESS_TOKEN”
–header “Content-Type: application/json”
–data ‘{
“resourceGroupId”: “grounding”,
“labels”: [
{
“key”: “ext.ai.sap.com/document-grounding”,
“value”: “true”
}
]
}’You can choose any valid ID for resourceGroupId (3 to 253 characters, letters, numbers, . and – allowed, must start and end with a letter or number). Substitute it wherever this post shows grounding. If you have an existing resource group you want to reuse, send a PATCH request to the same endpoint with just the labels array to add the grounding label.Step 2: Create the BTP DestinationThis destination replaces the reserved shared grounding JOULE_GLOBAL_DOC_GROUNDING alias used in Post 2. The BTP destination service handles the OAuth token lifecycle. Your capability YAML simply references the destination name.Open Connectivity > Destinations in your BTP Cockpit and create a new destination with these properties:PropertyValueNameAICORE_PRIVATE_GROUNDINGTypeHTTPURL<AI_API_URL> from the service binding credentialsProxy TypeInternetAuthenticationOAuth2ClientCredentialsClient ID<clientid> from the service binding credentialsClient Secret<clientsecret> from the service binding credentialsToken Service URL<url>/oauth/token from the service binding credentialsSave the destination and click Check Connection. A successful response confirms BTP can reach the AI Core API with valid credentials.This is the same pattern used for any OAuth2-protected SAP service. The destination abstracts authentication entirely. When your Joule capability sends a request through this destination, BTP automatically attaches a valid Bearer token.Step 3: Configure the Google Drive Connector in AI CoreThe following section covers the setup for document grounding with Google Drive. However, the same can be done via other allowed knowledge sourcs, such as Microsoft SharePoint.AI Core’s document grounding service uses generic secrets to hold connector credentials. Think of a generic secret as the AI Core equivalent of a BTP destination. It stores the connection details for a data source.Create a generic secret holding your GCP service account key. The endpoint is /v2/admin/secrets and all credential values must be base64-encoded. You can base64-encode a value on the command line with echo -n “<value>” | base64. curl –request POST
–url “<AI_API_URL>/v2/admin/secrets”
–header “Authorization: Bearer <access_token>”
–header “AI-Resource-Group: grounding”
–header “Content-Type: application/json”
–data-raw ‘{
“name”: “gdrive-compensation”,
“data”: {
“clientId”: “<BASE64_ENCODED_CLIENT_ID>”,
“private_key”: “<BASE64_ENCODED_PRIVATE_KEY>”,
“client_email”: “<BASE64_ENCODED_CLIENT_EMAIL>”,
“auth_uri”: “<BASE64_ENCODED_AUTH_URI>”,
“authentication”: “T0F1dGgyQ2xpZW50Q3JlZGVudGlhbHM=”,
“url”: “aHR0cHM6Ly9nb29nbGVhcGlzLmNvbS9kcml2ZS92Mw==”,
“tokenServiceURL”: “aHR0cHM6Ly9vYXV0aDIuZ29vZ2xlYXBpcy5jb20vdG9rZW4=”
},
“labels”: [
{
“key”: “ext.ai.sap.com/document-grounding”,
“value”: “true”
},
{
“key”: “ext.ai.sap.com/documentRepositoryType”,
“value”: “GoogleDrive”
}
]
}’The authentication, url, and tokenServiceURL values above are fixed base64-encoded constants. Do not change them. Only clientId, private_key, client_email, and auth_uri come from your GCP service account key JSON.The name field (gdrive-compensation) is what you reference in the pipeline creation request. This is distinct from the BTP destination name.Once the generic secret is created, create the pipeline pointing at your compensation documents folder:curl
–request POST
–url “<AI_API_URL>/v2/lm/document-grounding/pipelines”
–header “Authorization: Bearer <access_token>”
–header “AI-Resource-Group: grounding”
–header “content-type: application/json”
–data ‘{
“type”: “GoogleDrive”,
“configuration”: {
“destination”: “gdrive-compensation”,
“googleDrive”: {
“resourceType”: “SHARED_FOLDER”,
“resourceId”: “<your-compensation-folder-id>”
}
}
}’Note that configuration.destination refers to the generic secret name (gdrive-compensation), not the BTP destination name. The naming overlap is unfortunate but they are separate systems.If you want to index only specific subfolders rather than the entire shared folder, add an includePaths array inside googleDrive. For example, “includePaths”: [“CompensationDocs/2025″] would limit ingestion to that subfolder only.”googleDrive”: {
“resourceType”: “SHARED_FOLDER”,
“resourceId”: “<your-compensation-folder-id>”,
“includePaths”: [
“CompensationDocs/2025”
]
}The response includes a pipelineId. Save it. Poll the pipeline status until it returns FINISHED:curl
–request GET
–url “<AI_API_URL>/v2/lm/document-grounding/pipelines/<pipelineId>”
–header “Authorization: Bearer <access_token>”
–header “AI-Resource-Group: grounding”Ingestion for a small document set typically completes within a few minutes. For this example, upload the three sample compensation documents from the joule-dev-blog-samples GitHub repository under Blog_03_private_grounding/sample-documents/ to your Google Drive folder.Step 4: Create the CapabilityCreate this folder structure inside your capabilities/ directory:capabilities/
private_grounding/
capability.sapdas.yaml
scenarios/
search_compensation.yaml
functions/
search_compensation.yamlStart with capability.sapdas.yaml:schema_version: 3.28.0

metadata:
namespace: joule.ext
name: hr_compensation_search_capability
version: 1.0.0
display_name: HR Compensation Search
description: Search confidential HR compensation policies grounded in private AI Core documents

system_aliases:
AICORE_PRIVATE_GROUNDING:
destination: AICORE_PRIVATE_GROUNDINGUnlike Post 2’s JOULE_GLOBAL_DOC_GROUNDING, the destination name here (AICORE_PRIVATE_GROUNDING) is a real BTP destination you created in Step 2. The Joule runtime will look up this destination by name, retrieve credentials, and use them when making the API call. You own this destination, so you can update it, move it, or rotate credentials without touching the capability YAML.Step 5: The ScenarioCreate scenarios/search_compensation.yaml:description: >
Search compensation policies, salary bands, bonus structures, and equity guidelines.
Retrieve grounded answers from confidential HR compensation documents.

slots:
– name: query
description: The compensation policy question to search for
target:
name: search_compensation
type: function

response_context:
– value: $target_result.jouleResponse
description: Joule answer to grounding queryThe description covers the full range of compensation topics your documents address. Including concrete terms (“salary bands”, “bonus structures”, “equity guidelines”) improves Joule’s intent matching for this scenario. The response_context exposes the jouleResponse variable produced by the dialog function (defined in the next step) to Joule’s LLM for response generation.Optional: Restricting the scenario by user groupThe capability above already isolates the data. Only this capability can reach the AI Core pipeline, and Joule information retrieval cannot. That is the data-isolation layer. You can add a second, complementary layer that controls which users are allowed to trigger the scenario in the first place. Joule supports this declaratively through a visibility_condition block on the scenario:visibility_condition:
objects:
– type: ias_attributes
attributes:
– attribute: groups
match: ANY
values:
– HR_MANAGERSAdding this block to the scenario above gates it to users in the IAS group HR_MANAGERS. A user outside that group cannot trigger this scenario at all, even though they would otherwise be able to. Combined with the private pipeline, this gives you a defence-in-depth model. The data is reachable only through this capability, and this capability is reachable only by authorized users.visibility_condition has its own dedicated post in this series that walks through the full attribute model (e.g. groups or email), the ANY vs ALL match modes, IAS group setup, and how to combine multiple condition objects. We will not configure it on the deployed capability in this post so that you can test the data-isolation layer in isolation.Step 6: The Dialog FunctionCreate functions/search_compensation.yaml:parameters:
– name: query
optional: false
action_groups:
– actions:
– type: api-request
method: POST
headers:
content-type: application/json
AI-Resource-Group: grounding
system_alias: AICORE_PRIVATE_GROUNDING
path: “/v2/lm/document-grounding/retrieval/search”
body: >
{
“query”: “<? query ?>”,
“filters”: [
{
“id”: “compensation-filter”,
“searchConfiguration”: {
“maxChunkCount”: 3
},
“dataRepositories”: [“*”],
“dataRepositoryType”: “vector”,
“documentMetadata”: [],
“chunkMetadata”: []
}
]
}
result_variable: search_results
– type: set-variables
variables:
– name: jouleResponse
value: “The result for the given userQuery – <? query ?> is: <? search_results.body ?>”

result:
jouleResponse: <? jouleResponse ?>Comparing this to Post 2’s function, three things changed:system_alias: AICORE_PRIVATE_GROUNDING replaces JOULE_GLOBAL_DOC_GROUNDING. This tells the runtime to use your BTP destination instead of the reserved alias.path: “/v2/lm/document-grounding/retrieval/search” is the full AI Core API path. With the shared grounding service, the path was /retrieval/api/v1/search because the alias already pointed at the grounding service base URL. Here the destination URL is the AI Core root, so the full path is required.AI-Resource-Group: grounding is a header that AI Core requires on every document grounding API call. It scopes the request to the resource group you created in Step 1. If you used a different ID for your resource group, substitute it here.The retrieval body, maxChunkCount: 3, and the jouleResponse pattern follow the same logic as Post 2. Fetch the top three chunks and let Joule’s LLM pick the most relevant content for the user’s question. The AI Core retrieval endpoint returns the same response structure as the shared grounding service.Step 7: Deploy and TestValidate and deploy:joule lint
joule deploy -c -n “hr_compensation_test”
joule launch “hr_compensation_test”Test the capability inside the hr_compensation_test test instance you launched above. Try these queries:”What are the salary bands for senior engineers?””What is the bonus structure for IC3 level?””How does equity vesting work at ACME?””What are the RSU grant ranges for principal level?”Now verify the isolation. Switch from the hr_compensation_test window to the central sap_digital_assistant Joule and ask the same question directly without going through your capability: “What are the salary bands at ACME?” You should get a response indicating no relevant documents were found. The compensation documents are not in the shared index, so central Joule cannot surface them. The capability is the only path to the data.If the compensation search capability returns no results, check the resource group header. The AI-Resource-Group value in the function must match the resource group where you created the pipeline.Comparing the Two ApproachesYou now have two document grounding capabilities in your Digital Assistant:Product Compliance (Post 2) Northwind product data combined with compliance guidelines from the shared grounding service, accessible to all employees through your capability and through Joule.HR Compensation Search (this post): Confidential compensation data in a private AI Core pipeline, reachable only through this capability. Add a visibility_condition (covered in Post 4) to additionally restrict who can trigger the scenario.Both capabilities coexist in the same Digital Assistant without conflict. The shared grounding alias and the AI Core destination are independent. Each capability handles its own retrieval path.The decision between shared and private grounding comes down to one question. Should the information/document be available to all Joule users? If yes, shared grounding is simpler to set up and requires no BTP destination. If no, private grounding is the only option that provides actual data isolation.A few additional guidelines:Shared grounding works well for anything you would put on an internal wiki. Examples include office information, general HR policies, IT guides, and onboarding materials.Private grounding is appropriate for any content where unauthorized access would be a compliance, legal, or business problem. Examples include compensation data, financial projections, M&A documents, legal analysis, and board materials.You can run multiple private pipelines in the same AI Core instance and serve them from multiple capabilities. Each capability can independently add a visibility_condition once you have read Post 4: Role-Based Scenario Access.If your data repository is Microsoft SharePoint, a third security option exists: per-document access control that mirrors SharePoint ACLs into the grounding index, so two users hitting the same scenario each see only the documents they are personally permitted to view in SharePoint. See “What This Post Does Not Cover” below.What This Post Does Not CoverThe capability in this post uses the minimum surface of the AI Core grounding module needed to make the data-isolation point: one pipeline, one filter, three chunks, and Joule response generation. The same setup sits on top of a much larger grounding API that this post does not exercise. For a real-world HR or compliance corpus you will likely want some of the following.Per-document metadata via a Metadata API Server. AI Core can call an OAuth2-protected metadata endpoint during pipeline ingestion to attach custom keys to each document, for example department, region, level, or domainId. Retrieval can then be scoped to documents for region=EMEA and level=IC5 rather than everything in the pipeline. See Prepare your Metadata API Server.Multi-level metadata filtering and complex match expressions. The /retrieval/search payload accepts filters at the data-repository, document, and chunk levels, with ANY and ALL match modes and nested AND/OR expressions. This blog post sets all three filter arrays to empty. See Vector Search.Post-processing and reranking. The retrieval endpoint accepts a postProcessing block that can merge results across multiple filters or reorder them using the Cohere 3.5 reranker, including metadata-based boosting. For three sample documents, raw vector similarity is enough. For a larger compensation corpus that mixes salary bands, bonus structures, and equity policies, reranking and boosting on the user’s level or region make a measurable difference to result quality. See Retrieval Search.Other ingestion paths. The same grounding module supports Microsoft SharePoint, AWS S3, SFTP, SAP Build Work Zone, SAP Document Management service, and ServiceNow alongside Google Drive. Per-document access control (Microsoft SharePoint only). When the data repository is Microsoft SharePoint, you can set accessControlEnabled: true on the pipeline. The grounding service then mirrors SharePoint’s per-document permissions into the index, including Entra ID security groups, direct user assignments, and inherited folder permissions. At query time, document grounding checks the signed-in user’s identity against those permissions and returns only the documents that user is allowed to see in SharePoint. This gives you a third, per-document layer that sits on top of the data-isolation layer in this post and the scenario-level visibility_condition in Post 4. It also requires admin consent for the Group.Read.All and User.Read.All Microsoft Graph permissions, the user’s IAS email must match their Entra ID email, and the setting is one-way (it cannot be turned off once enabled). At the time of writing it is available only on AWS eu10 and Google Cloud eu30. See Enable Document Access Control for Microsoft SharePoint.SAP AI Launchpad. Pipelines, generic secrets, collections, documents, and executions in your AI Core resource group can be inspected and managed in the SAP AI Launchpad UI. The curl flow in this post is the developer setup path. Once the capability is wired, the people maintaining the document corpus do not need to touch curl.None of these require changes to the capability YAML. Only the retrieval payload and the data and metadata behind it get richer.ConclusionBuilding Custom Joule Capabilities: Document Grounding  established document grounding access to the shared grounding instance . This post covered an option to use document grounding only in specific capabilities . By moving your pipeline into AI Core and backing the system alias with a real BTP destination, you control who can reach the documents.The capability code barely changes. The real work is infrastructure. You need an AI Core service key, a BTP destination, a generic secret in AI Core, and a pipeline pointing at a Google Drive folder. Once those are in place, the data is reachable only through this capability. Layering a visibility_condition on top of that, as covered in Post 4 – Building Custom Joule Capabilities: Role-Based Scenario Access in Joule, gives you the genuine two-layer model, with data isolation at the storage layer and flow control at the scenario layer.In the next post, we look at gating the scenario itself by user attributes using visibility_condition and IAS group membership.    Read More Technology Blog Posts by SAP articles 

#SAP

#SAPTechnologyblog

You May Also Like

More From Author