Building Custom Joule Capabilities: Document Grounding
Share

[[{“value”:”

This is part of a series on building custom SAP Joule capabilities. Start with Building Custom Joule Capabilities: Getting Started if you have not set up your environment yet. If you are here to learn about accessing document grounding from a Joule capability and what you can do with it, you can read this post on its own without going through the full series.

Many business questions cannot be answered from live backend data alone. A user asking “What are the compliance requirements for Chang?” (Chang is a beer in the Northwind catalog) needs the product’s category from the catalog and the actual compliance rules for that category from internal documentation. That documentation lives in company-specific documents, not in an OData service. Without a way to query those documents at runtime, the model is left to generate an answer from its training data alone, which risks hallucination on company-specific content, or the capability can only point the user somewhere else.

This post covers how a Joule capability can make use of company-specific documents already uploaded to Joule’s grounding service to answer this kind of question. The capability covered here first fetches product details from the Northwind OData service, then uses the product’s category to query the document grounding service, and finally surfaces a combined response with both live data and grounded compliance excerpts.

The flow is:

User question ──► Joule slot extraction ──► OData (Northwind)
                                                  │
                                                  ▼
                                          Category resolved
                                                  │
                                                  ▼
                                  Targeted grounding query
                                  (category + topic keywords)
                                                  │
                                                  ▼
                                       Document grounding
                                                  │
                                                  ▼
                                Joule response generation ──► User

What is Document Grounding (RAG)?

The technique behind this is called retrieval-augmented generation (RAG). Instead of relying solely on the language model’s training data, you retrieve relevant text from your own documents at query time and include that text in the context the model uses to generate a response.

The Joule document grounding service handles the infrastructure: it ingests your documents, splits them into chunks, generates vector embeddings, and performs similarity search when a query arrives. You configure a pipeline and upload documents to Google Drive (or other data sources such as Microsoft SharePoint) in order to retrieve grounded information.


Document Grounding in Joule vs. Capability Grounding Search

Before building the capability, it is worth understanding what the capability adds over Document Grounding in Joule.

When you index documents into the document grounding service, the central Joule instance can already search them automatically. A user can ask “What are the Beverages compliance rules?” and Joule returns a sourced answer with no capability involved or required. The Google Drive setup blog post demonstrates exactly this and is also the starting point to use document grounding from within a capability itself. One of the advantages of Document Grounding in Joule is the references to the grounded documents that are provided as well as a well-written answer to the query.

Direct grounding works well only when the user’s question already carries enough semantic signal. To see how that plays out, it helps to compare two products from the Northwind catalog that play different roles. Chai is a tea-based beverage. Chang is a beer. Both are in the Beverages category, so both share the same compliance rules. Their names behave very differently under vector search.

Document Grounding in Joule works well when the user’s question already contains enough semantic signal. Ask “What are the compliance requirements for Chai?” and the vector search embeds “Chai” strongly into the tea/hot drink/beverage neighborhood, which is close enough to retrieve the Beverages section. For many product names, this is sufficient.

But there is a class of questions where it breaks down. Ask “What are the compliance requirements for Chang?” and even if the vector search retrieves something beverage-related, it cannot know which product category to scope the compliance lookup to. Chang is a beer, which falls under the Beverages category, but nothing in the user’s question tells the grounding service that. Without first resolving the product from the catalog and reading its category, the document query has no precise signal to work with. The result is either nothing retrieved, or retrieved content that does not match the actual product.

The screenshot below shows the contrast in central Joule. The Chai query returns the Beverages compliance section. The Chang query, asked moments later, falls back to a generic non-answer.

joule-chai-vs-chang-comparison.png

This is not a quirk of one product name. It reflects a broader pattern. Whenever the user’s phrasing lacks inherent semantic content, raw vector search on the user’s words will underperform or fail. A few real SAP examples where the same issue appears:

  • Opaque internal IDs. A user asks “What is the maintenance schedule for asset 4711?” The asset ID has no semantic meaning. Without an OData call to the Asset Master to resolve 4711 to “Cooling Pump, hazardous-area,” the document search has no signal to work with.
  • Ambiguous names. A field technician asks about “compliance for the L-200.” That could be a vehicle, a pump, or a chemical compound. Live backend data resolves which one. The user’s words alone cannot.
  • Role-dependent answers. “What can I approve?” is meaningless to a vector search. The answer depends on the current user’s authorization profile, which must be fetched at runtime before the document query can be scoped correctly.

A capability with document grounding solves this by chaining. First it fetches the structured backend data that gives the question its meaning. Then it uses that data to construct a targeted search query.

  Document Grounding in Joule Capability with Grounding API
Trigger Any information retrieval question Specific scoped intent
Query source User’s raw question Can be constructed (e.g. live backend data)
Chaining Not possible Chain OData, APIs, and logic with document retrieval
Scoping All indexed documents Can be filtered by pipeline metadata
Retrieval control Fixed defaults Configurable (e.g., number of chunks returned)
Response format Joule’s default cited format with reference links Custom template or Joule response generation

The capability in this post first calls Northwind OData to get the product’s category, then constructs a category-specific compliance query (“Beverages product compliance safety requirements”), and sends that targeted query to the grounding service. The document search is informed by live data.


Prerequisites

Before you begin, make sure you have:


Step 1: Prepare the Compliance Document

For this post, we use a sample compliance document called product-compliance-guidelines.md. It contains compliance sections organized by Northwind product category, covering Beverages, Seafood, Condiments, Dairy Products, and Grains and Cereals. You can find it in the joule-dev-blog-samples GitHub repository under Blog_02_document_grounding/.

In practice, you would use your own documents here. The capability works with any document content indexed in a grounding pipeline. What matters for the capability to work is that a pipeline exists and the documents are indexed.

To follow along with the sample, two things need to happen.

1. Upload the document. Since the grounding service does not support Markdown files directly, create a new Google Docs document in the Drive folder connected to your grounding service account, copy the full content of product-compliance-guidelines.md into it, and save it. You can also ingest the document via a Microsoft SharePoint pipeline if your grounding instance is configured for SharePoint access. The metadata tag introduced in step 2 is the same in both cases.

2. Create the pipeline with a metadata tag. Point the pipeline at the Drive folder (or SharePoint location) and add the metadata tag pipeline/documents: product-compliance so the capability can scope searches to it later. If your grounding instance only ever serves this one document set, the metadata tag is optional. If it serves multiple document sets, the tag is what keeps this capability isolated from the rest:

{
“type”: “GoogleDrive”,
“configuration”: {
“destination”: “<your-destination-name>”,
“googleDrive”: {
“resourceType”: “SHARED_FOLDER”,
“resourceId”: “<your-folder-id>”
}
},
“metadata”: {
“dataRepositoryMetadata”: [
{
“key”: “pipeline/documents”,
“value”: [“product-compliance”]
}
]
}
}

Wait for ingestion to complete before deploying. The pipeline status endpoint returns FINISHED when indexing is done.

bruno-pipeline-status-finished.png


Step 2: Create the Capability

Create this folder structure inside your capabilities/ directory:

capabilities/
  product_compliance/
    capability.sapdas.yaml
    scenarios/
      check_compliance.yaml
    functions/
      check_compliance.yaml

Start with capability.sapdas.yaml:

schema_version: 3.28.0

metadata:
namespace: joule.ext
name: product_compliance_capability
version: 1.0.0
display_name: Product Compliance
description: >
Look up compliance and safety requirements for Northwind products.
Fetches live product details and retrieves relevant compliance guidelines
from grounded documents.

system_aliases:
NorthwindService:
destination: Northwind
JOULE_GLOBAL_DOC_GROUNDING:
destination: JOULE_GLOBAL_DOC_GROUNDING

This capability declares two system aliases. NorthwindService points to the same Northwind BTP destination from the first post of this series. JOULE_GLOBAL_DOC_GROUNDING is the reserved alias the Joule runtime recognizes and routes to the document grounding service. The destination value must match exactly: JOULE_GLOBAL_DOC_GROUNDING. There is no BTP destination to create and no service key to manage on the capability side. Make also sure to use joule.ext as namespace.


Step 3: The Scenario

Create scenarios/check_compliance.yaml:

description: >
Look up compliance requirements, safety guidelines, storage rules, and handling
procedures for a specific product. Ask about compliance for Chang, safety
requirements for Konbu, handling guidelines for Queso Cabrales, or storage
rules for Chai.
slots:
– name: product_name
description: The name of the product to check compliance requirements for

target:
name: check_compliance
type: function

response_context:
– value: $target_result.jouleResponse
description: Product category and grounded guidelines

The description field scopes this scenario specifically to product compliance questions. Including concrete product examples (“Chang”, “Konbu”, “Queso Cabrales”, “Chai”) alongside topic keywords gives Joule’s intent matching clear signal. This reduces overlap with Document Grounding in Joule, which activates on broader information retrieval questions but does not match product-specific compliance intents as precisely.

The product_name slot captures the product name from the user’s question. When someone asks “What are the compliance requirements for Chang?”, Joule extracts “Chang” and passes it to the dialog function.

The response_context block tells Joule what data to use when generating the response. Rather than rendering a hardcoded template, Joule’s LLM synthesizes a natural language answer from the value provided here. The jouleResponse variable bundles the user input, the resolved product category, and the full grounding response together in a single string. The reasons for this are covered in the next section.


Step 4: The Dialog Function

The function runs two sequential API calls (Northwind, then document grounding) split across three action groups. The middle action group handles the not-found case so the second API call only runs when the first one returned a product. Create functions/check_compliance.yaml:

parameters:
– name: product_name
optional: false

action_groups:
– actions:
– type: api-request
method: GET
system_alias: NorthwindService
path: “/Products?$filter=contains(tolower(ProductName),tolower(‘<? product_name ?>’))&$expand=Category,Supplier&$top=1”
result_variable: product_result
– type: set-variables
scripting_type: spel
variables:
– name: category_name
value: <? product_result.body.value[0].Category.CategoryName ?>

– condition: “product_result.status_code != 200 || product_result.body.value == null || product_result.body.value.size() == 0”
actions:
– type: message
message:
type: text
content: “No product matching ‘<? product_name ?>’ was found in the catalog.”

– condition: “product_result.status_code == 200 && product_result.body.value != null && product_result.body.value.size() > 0”
actions:
– type: api-request
method: POST
headers:
content-type: application/json
system_alias: JOULE_GLOBAL_DOC_GROUNDING
path: “/retrieval/api/v1/search”
body: >
{
“query”: “<? category_name ?> product compliance safety requirements”,
“filters”: [
{
“id”: “compliance-filter”,
“searchConfiguration”: {
“maxChunkCount”: 3
},
“dataRepositories”: [“*”],
“dataRepositoryType”: “vector”,
“dataRepositoryMetadata”: [
{“key”: “pipeline/documents”, “value”: [“product-compliance”]}
],
“documentMetadata”: [],
“chunkMetadata”: []
}
]
}
result_variable: search_results
– type: set-variables
variables:
– name: jouleResponse
value: “The result for the given userQuery – <? product_name ?> in category <? category_name ?> is: <? search_results.body ?>”

result:
jouleResponse: <? jouleResponse ?>

Let us walk through the key parts.

First action group: Fetch the product from Northwind

The first api-request calls the Northwind OData service. The $filter uses a case-insensitive contains so “chang” or “Chan” matches “Chang”. $expand=Category,Supplier navigates the OData navigation properties to include the full Category and Supplier objects in the response. Without $expand, you get only the CategoryID and SupplierID foreign keys, not the names.

The set-variables action extracts the category name from the response using SpEL. This is the field used to construct the grounding query in the next action group.

Second action group: Handle the not-found case

The second action_groups entry has a condition that fires when the product fetch returned no results or an error. It sends a simple message back to the user rather than attempting the document search with empty variables.

Third action group: Search compliance documents

The third action_groups entry fires only when the product fetch succeeded. The document grounding api-request constructs its query from the category_name variable:

“query”: “<? category_name ?> product compliance safety requirements”

This is the core of the chaining pattern. The query the grounding service receives is not “compliance for Chang” (the user’s raw question). It is “Beverages product compliance safety requirements” (derived from live catalog data). The vector search finds the Beverages section of the compliance document rather than trying to match on the product name directly.

The dataRepositoryMetadata filter scopes the search to pipelines tagged with pipeline/documents: product-compliance, keeping this search isolated from any other document sets in your grounding instance.

maxChunkCount: 3 tells the grounding service to return the top three most relevant chunks. We hand all three to Joule’s LLM along with the resolved category and let it pick the section that actually matches. The reasons for this choice are explained in the next section.

Result block: exposing data for response generation

Instead of a hardcoded message action, the function ends with a result block that exposes a single jouleResponse string to the scenario’s response_context. The string includes the user’s product input, the resolved category, and the full grounding response body. Joule’s LLM then synthesizes the final response from that combined context.


Why Response Generation Instead of a Hardcoded Template

An alternative would be to use a hardcoded message template that renders the chunk text directly. However, that approach runs into a problem with how the document grounding service returns chunks.

The compliance document contains sections for multiple product categories such as Beverages, Seafood, Condiments, and others. Each section is split into chunks during ingestion. The vector similarity search scores chunks by relevance to the query. However, the chunks are not cleanly separated by category and one chunk itself could contain parts of multiple categories (e.g. Seafood and beverages compliance). The result is a response that presents Beverages product information alongside compliance rules from a different category.

The screenshot below shows this happening. The query was about Konbu (Seafood), but the top-scored chunk contained alcohol compliance content from the Beverages section:

joule-konbu-wrong-chunk-beverages.png

Here is the information/chunk highlighted in the document:

gdoc-compliance-guidelines-chunk.png

Furthermore, if retrieving multiple chunks, a query for “Beverages product compliance” may score a Seafood chunk highly if the text has overlapping vocabulary (handling requirements, storage conditions, safety standards). Even with maxChunkCount: 1, the single top-scored chunk is usually correct, but not always.

Switching to response generation with response_context solves this. Instead of displaying the raw chunk text directly, we set maxChunkCount: 3 and pass the full grounding response body, framed with the user’s product query and the resolved category, to Joule’s LLM in a single jouleResponse string. With three candidate chunks plus the explicit category context, the LLM has enough signal to pick the section that actually matches and discard chunks from a neighboring category.

joule-konbu-right.png

Same query, same document, different result. The LLM saw three candidate chunks plus the explicit “Konbu in category Seafood” framing and discarded the alcohol compliance chunk on relevance grounds.

The tradeoff is that you give up precise control over formatting. A hardcoded template lets you design the exact layout, including headers, bold labels, bullet points (markdown), and even UI integration cards. Response generation produces coherent, readable output, but the structure is determined by Joule. For compliance information where accuracy matters more than visual structure, response generation is the better choice.


Step 5: Deploy and Test

Validate the YAML first:

joule lint

If linting passes, deploy as a standalone test assistant:

joule deploy -c -n “product_compliance_test”

Open the test client:

joule launch “product_compliance_test”

Try these queries:

  • “What are the compliance requirements for Chang?” Expected: Beverages compliance guidelines (Chang is a beer in Northwind, resolved via OData → category → grounded retrieval)
  • “Show me safety guidelines for Konbu” Expected: Seafood compliance guidelines generated by Joule

If the grounding search returns no results, verify two things: the pipeline metadata (pipeline/documents: product-compliance in the request must match the tag you set on the pipeline) and that ingestion has completed (pipeline status is FINISHED).

If the OData call returns no product, check the product name spelling and confirm the Northwind BTP destination is correctly configured.


Conclusion

The JOULE_GLOBAL_DOC_GROUNDING alias and the /retrieval/api/v1/search endpoint give you a searchable document layer inside any dialog function. What makes this useful is not document retrieval on its own (Document Grounding in Joule already does that) but chaining. The capability fetches live data from a backend system, uses that data to construct a targeted search query, and surfaces the results through Joule’s response generation.

The product compliance capability demonstrates the pattern cleanly. A product name from the user maps to a category from OData, which maps to a compliance section in the document index. Joule’s LLM then synthesizes a coherent response from the retrieved chunk, filtering out any content that does not match the context. A hardcoded template cannot do that.

One limitation remains. The shared grounding service is accessible to all users through the central sap_digital_assistant. Documents indexed into a shared pipeline can be retrieved by anyone asking the central Joule directly, regardless of which capability they used. For general product guidelines this is usually acceptable. For sensitive content, such as internal pricing policies, salary bands, or legal documents, you need data isolation at the storage layer, not just at the capability level.

The next blog post,  Building Custom Joule Capabilities: Private Document Grounding with SAP AI Core , addresses this directly. It moves the grounding pipeline to your own SAP AI Core instance so that documents are only reachable through your capability, with no path through central Joule.

“}]] 

 [[{“value”:”This is part of a series on building custom SAP Joule capabilities. Start with Building Custom Joule Capabilities: Getting Started if you have not set up your environment yet. If you are here to learn about accessing document grounding from a Joule capability and what you can do with it, you can read this post on its own without going through the full series.Many business questions cannot be answered from live backend data alone. A user asking “What are the compliance requirements for Chang?” (Chang is a beer in the Northwind catalog) needs the product’s category from the catalog and the actual compliance rules for that category from internal documentation. That documentation lives in company-specific documents, not in an OData service. Without a way to query those documents at runtime, the model is left to generate an answer from its training data alone, which risks hallucination on company-specific content, or the capability can only point the user somewhere else.This post covers how a Joule capability can make use of company-specific documents already uploaded to Joule’s grounding service to answer this kind of question. The capability covered here first fetches product details from the Northwind OData service, then uses the product’s category to query the document grounding service, and finally surfaces a combined response with both live data and grounded compliance excerpts.The flow is:User question ──► Joule slot extraction ──► OData (Northwind)


Category resolved


Targeted grounding query
(category + topic keywords)


Document grounding


Joule response generation ──► UserWhat is Document Grounding (RAG)?The technique behind this is called retrieval-augmented generation (RAG). Instead of relying solely on the language model’s training data, you retrieve relevant text from your own documents at query time and include that text in the context the model uses to generate a response.The Joule document grounding service handles the infrastructure: it ingests your documents, splits them into chunks, generates vector embeddings, and performs similarity search when a query arrives. You configure a pipeline and upload documents to Google Drive (or other data sources such as Microsoft SharePoint) in order to retrieve grounded information.Document Grounding in Joule vs. Capability Grounding SearchBefore building the capability, it is worth understanding what the capability adds over Document Grounding in Joule.When you index documents into the document grounding service, the central Joule instance can already search them automatically. A user can ask “What are the Beverages compliance rules?” and Joule returns a sourced answer with no capability involved or required. The Google Drive setup blog post demonstrates exactly this and is also the starting point to use document grounding from within a capability itself. One of the advantages of Document Grounding in Joule is the references to the grounded documents that are provided as well as a well-written answer to the query.Direct grounding works well only when the user’s question already carries enough semantic signal. To see how that plays out, it helps to compare two products from the Northwind catalog that play different roles. Chai is a tea-based beverage. Chang is a beer. Both are in the Beverages category, so both share the same compliance rules. Their names behave very differently under vector search.Document Grounding in Joule works well when the user’s question already contains enough semantic signal. Ask “What are the compliance requirements for Chai?” and the vector search embeds “Chai” strongly into the tea/hot drink/beverage neighborhood, which is close enough to retrieve the Beverages section. For many product names, this is sufficient.But there is a class of questions where it breaks down. Ask “What are the compliance requirements for Chang?” and even if the vector search retrieves something beverage-related, it cannot know which product category to scope the compliance lookup to. Chang is a beer, which falls under the Beverages category, but nothing in the user’s question tells the grounding service that. Without first resolving the product from the catalog and reading its category, the document query has no precise signal to work with. The result is either nothing retrieved, or retrieved content that does not match the actual product.The screenshot below shows the contrast in central Joule. The Chai query returns the Beverages compliance section. The Chang query, asked moments later, falls back to a generic non-answer.This is not a quirk of one product name. It reflects a broader pattern. Whenever the user’s phrasing lacks inherent semantic content, raw vector search on the user’s words will underperform or fail. A few real SAP examples where the same issue appears:Opaque internal IDs. A user asks “What is the maintenance schedule for asset 4711?” The asset ID has no semantic meaning. Without an OData call to the Asset Master to resolve 4711 to “Cooling Pump, hazardous-area,” the document search has no signal to work with.Ambiguous names. A field technician asks about “compliance for the L-200.” That could be a vehicle, a pump, or a chemical compound. Live backend data resolves which one. The user’s words alone cannot.Role-dependent answers. “What can I approve?” is meaningless to a vector search. The answer depends on the current user’s authorization profile, which must be fetched at runtime before the document query can be scoped correctly.A capability with document grounding solves this by chaining. First it fetches the structured backend data that gives the question its meaning. Then it uses that data to construct a targeted search query. Document Grounding in JouleCapability with Grounding APITriggerAny information retrieval questionSpecific scoped intentQuery sourceUser’s raw questionCan be constructed (e.g. live backend data)ChainingNot possibleChain OData, APIs, and logic with document retrievalScopingAll indexed documentsCan be filtered by pipeline metadataRetrieval controlFixed defaultsConfigurable (e.g., number of chunks returned)Response formatJoule’s default cited format with reference linksCustom template or Joule response generationThe capability in this post first calls Northwind OData to get the product’s category, then constructs a category-specific compliance query (“Beverages product compliance safety requirements”), and sends that targeted query to the grounding service. The document search is informed by live data.PrerequisitesBefore you begin, make sure you have:Joule Studio CLI installed and authenticated (see Building Custom Joule Capabilities: Getting Started)Document grounding infrastructure set up on your BTP subaccount. This post uses Google Drive as the document source. Follow Setting Up Document Grounding with Google Drive on SAP BTP for the setup walkthrough. Alternatively, other data sources work just as well. For Microsoft SharePoint, see Nagesh Caparthy’s setup guide: Joule – Getting Started with Document Grounding – setup guide – 1.Documents uploaded to your data source and indexed in the pipeline (sample compliance document is provided in the sample repository, see Step 1)The Northwind BTP destination (Northwind) configured from Building Custom Joule Capabilities: Getting StartedStep 1: Prepare the Compliance DocumentFor this post, we use a sample compliance document called product-compliance-guidelines.md. It contains compliance sections organized by Northwind product category, covering Beverages, Seafood, Condiments, Dairy Products, and Grains and Cereals. You can find it in the joule-dev-blog-samples GitHub repository under Blog_02_document_grounding/.In practice, you would use your own documents here. The capability works with any document content indexed in a grounding pipeline. What matters for the capability to work is that a pipeline exists and the documents are indexed.To follow along with the sample, two things need to happen.1. Upload the document. Since the grounding service does not support Markdown files directly, create a new Google Docs document in the Drive folder connected to your grounding service account, copy the full content of product-compliance-guidelines.md into it, and save it. You can also ingest the document via a Microsoft SharePoint pipeline if your grounding instance is configured for SharePoint access. The metadata tag introduced in step 2 is the same in both cases.2. Create the pipeline with a metadata tag. Point the pipeline at the Drive folder (or SharePoint location) and add the metadata tag pipeline/documents: product-compliance so the capability can scope searches to it later. If your grounding instance only ever serves this one document set, the metadata tag is optional. If it serves multiple document sets, the tag is what keeps this capability isolated from the rest:{
“type”: “GoogleDrive”,
“configuration”: {
“destination”: “<your-destination-name>”,
“googleDrive”: {
“resourceType”: “SHARED_FOLDER”,
“resourceId”: “<your-folder-id>”
}
},
“metadata”: {
“dataRepositoryMetadata”: [
{
“key”: “pipeline/documents”,
“value”: [“product-compliance”]
}
]
}
}Wait for ingestion to complete before deploying. The pipeline status endpoint returns FINISHED when indexing is done.Step 2: Create the CapabilityCreate this folder structure inside your capabilities/ directory:capabilities/
product_compliance/
capability.sapdas.yaml
scenarios/
check_compliance.yaml
functions/
check_compliance.yamlStart with capability.sapdas.yaml:schema_version: 3.28.0

metadata:
namespace: joule.ext
name: product_compliance_capability
version: 1.0.0
display_name: Product Compliance
description: >
Look up compliance and safety requirements for Northwind products.
Fetches live product details and retrieves relevant compliance guidelines
from grounded documents.

system_aliases:
NorthwindService:
destination: Northwind
JOULE_GLOBAL_DOC_GROUNDING:
destination: JOULE_GLOBAL_DOC_GROUNDINGThis capability declares two system aliases. NorthwindService points to the same Northwind BTP destination from the first post of this series. JOULE_GLOBAL_DOC_GROUNDING is the reserved alias the Joule runtime recognizes and routes to the document grounding service. The destination value must match exactly: JOULE_GLOBAL_DOC_GROUNDING. There is no BTP destination to create and no service key to manage on the capability side. Make also sure to use joule.ext as namespace.Step 3: The ScenarioCreate scenarios/check_compliance.yaml:description: >
Look up compliance requirements, safety guidelines, storage rules, and handling
procedures for a specific product. Ask about compliance for Chang, safety
requirements for Konbu, handling guidelines for Queso Cabrales, or storage
rules for Chai.
slots:
– name: product_name
description: The name of the product to check compliance requirements for

target:
name: check_compliance
type: function

response_context:
– value: $target_result.jouleResponse
description: Product category and grounded guidelinesThe description field scopes this scenario specifically to product compliance questions. Including concrete product examples (“Chang”, “Konbu”, “Queso Cabrales”, “Chai”) alongside topic keywords gives Joule’s intent matching clear signal. This reduces overlap with Document Grounding in Joule, which activates on broader information retrieval questions but does not match product-specific compliance intents as precisely.The product_name slot captures the product name from the user’s question. When someone asks “What are the compliance requirements for Chang?”, Joule extracts “Chang” and passes it to the dialog function.The response_context block tells Joule what data to use when generating the response. Rather than rendering a hardcoded template, Joule’s LLM synthesizes a natural language answer from the value provided here. The jouleResponse variable bundles the user input, the resolved product category, and the full grounding response together in a single string. The reasons for this are covered in the next section.Step 4: The Dialog FunctionThe function runs two sequential API calls (Northwind, then document grounding) split across three action groups. The middle action group handles the not-found case so the second API call only runs when the first one returned a product. Create functions/check_compliance.yaml:parameters:
– name: product_name
optional: false

action_groups:
– actions:
– type: api-request
method: GET
system_alias: NorthwindService
path: “/Products?$filter=contains(tolower(ProductName),tolower(‘<? product_name ?>’))&$expand=Category,Supplier&$top=1”
result_variable: product_result
– type: set-variables
scripting_type: spel
variables:
– name: category_name
value: <? product_result.body.value[0].Category.CategoryName ?>

– condition: “product_result.status_code != 200 || product_result.body.value == null || product_result.body.value.size() == 0”
actions:
– type: message
message:
type: text
content: “No product matching ‘<? product_name ?>’ was found in the catalog.”

– condition: “product_result.status_code == 200 && product_result.body.value != null && product_result.body.value.size() > 0”
actions:
– type: api-request
method: POST
headers:
content-type: application/json
system_alias: JOULE_GLOBAL_DOC_GROUNDING
path: “/retrieval/api/v1/search”
body: >
{
“query”: “<? category_name ?> product compliance safety requirements”,
“filters”: [
{
“id”: “compliance-filter”,
“searchConfiguration”: {
“maxChunkCount”: 3
},
“dataRepositories”: [“*”],
“dataRepositoryType”: “vector”,
“dataRepositoryMetadata”: [
{“key”: “pipeline/documents”, “value”: [“product-compliance”]}
],
“documentMetadata”: [],
“chunkMetadata”: []
}
]
}
result_variable: search_results
– type: set-variables
variables:
– name: jouleResponse
value: “The result for the given userQuery – <? product_name ?> in category <? category_name ?> is: <? search_results.body ?>”

result:
jouleResponse: <? jouleResponse ?>Let us walk through the key parts.First action group: Fetch the product from NorthwindThe first api-request calls the Northwind OData service. The $filter uses a case-insensitive contains so “chang” or “Chan” matches “Chang”. $expand=Category,Supplier navigates the OData navigation properties to include the full Category and Supplier objects in the response. Without $expand, you get only the CategoryID and SupplierID foreign keys, not the names.The set-variables action extracts the category name from the response using SpEL. This is the field used to construct the grounding query in the next action group.Second action group: Handle the not-found caseThe second action_groups entry has a condition that fires when the product fetch returned no results or an error. It sends a simple message back to the user rather than attempting the document search with empty variables.Third action group: Search compliance documentsThe third action_groups entry fires only when the product fetch succeeded. The document grounding api-request constructs its query from the category_name variable:”query”: “<? category_name ?> product compliance safety requirements”This is the core of the chaining pattern. The query the grounding service receives is not “compliance for Chang” (the user’s raw question). It is “Beverages product compliance safety requirements” (derived from live catalog data). The vector search finds the Beverages section of the compliance document rather than trying to match on the product name directly.The dataRepositoryMetadata filter scopes the search to pipelines tagged with pipeline/documents: product-compliance, keeping this search isolated from any other document sets in your grounding instance.maxChunkCount: 3 tells the grounding service to return the top three most relevant chunks. We hand all three to Joule’s LLM along with the resolved category and let it pick the section that actually matches. The reasons for this choice are explained in the next section.Result block: exposing data for response generationInstead of a hardcoded message action, the function ends with a result block that exposes a single jouleResponse string to the scenario’s response_context. The string includes the user’s product input, the resolved category, and the full grounding response body. Joule’s LLM then synthesizes the final response from that combined context.Why Response Generation Instead of a Hardcoded TemplateAn alternative would be to use a hardcoded message template that renders the chunk text directly. However, that approach runs into a problem with how the document grounding service returns chunks.The compliance document contains sections for multiple product categories such as Beverages, Seafood, Condiments, and others. Each section is split into chunks during ingestion. The vector similarity search scores chunks by relevance to the query. However, the chunks are not cleanly separated by category and one chunk itself could contain parts of multiple categories (e.g. Seafood and beverages compliance). The result is a response that presents Beverages product information alongside compliance rules from a different category.The screenshot below shows this happening. The query was about Konbu (Seafood), but the top-scored chunk contained alcohol compliance content from the Beverages section:Here is the information/chunk highlighted in the document:Furthermore, if retrieving multiple chunks, a query for “Beverages product compliance” may score a Seafood chunk highly if the text has overlapping vocabulary (handling requirements, storage conditions, safety standards). Even with maxChunkCount: 1, the single top-scored chunk is usually correct, but not always.Switching to response generation with response_context solves this. Instead of displaying the raw chunk text directly, we set maxChunkCount: 3 and pass the full grounding response body, framed with the user’s product query and the resolved category, to Joule’s LLM in a single jouleResponse string. With three candidate chunks plus the explicit category context, the LLM has enough signal to pick the section that actually matches and discard chunks from a neighboring category.Same query, same document, different result. The LLM saw three candidate chunks plus the explicit “Konbu in category Seafood” framing and discarded the alcohol compliance chunk on relevance grounds.The tradeoff is that you give up precise control over formatting. A hardcoded template lets you design the exact layout, including headers, bold labels, bullet points (markdown), and even UI integration cards. Response generation produces coherent, readable output, but the structure is determined by Joule. For compliance information where accuracy matters more than visual structure, response generation is the better choice.Step 5: Deploy and TestValidate the YAML first:joule lintIf linting passes, deploy as a standalone test assistant:joule deploy -c -n “product_compliance_test”Open the test client:joule launch “product_compliance_test”Try these queries:”What are the compliance requirements for Chang?” Expected: Beverages compliance guidelines (Chang is a beer in Northwind, resolved via OData → category → grounded retrieval)”Show me safety guidelines for Konbu” Expected: Seafood compliance guidelines generated by JouleIf the grounding search returns no results, verify two things: the pipeline metadata (pipeline/documents: product-compliance in the request must match the tag you set on the pipeline) and that ingestion has completed (pipeline status is FINISHED).If the OData call returns no product, check the product name spelling and confirm the Northwind BTP destination is correctly configured.ConclusionThe JOULE_GLOBAL_DOC_GROUNDING alias and the /retrieval/api/v1/search endpoint give you a searchable document layer inside any dialog function. What makes this useful is not document retrieval on its own (Document Grounding in Joule already does that) but chaining. The capability fetches live data from a backend system, uses that data to construct a targeted search query, and surfaces the results through Joule’s response generation.The product compliance capability demonstrates the pattern cleanly. A product name from the user maps to a category from OData, which maps to a compliance section in the document index. Joule’s LLM then synthesizes a coherent response from the retrieved chunk, filtering out any content that does not match the context. A hardcoded template cannot do that.One limitation remains. The shared grounding service is accessible to all users through the central sap_digital_assistant. Documents indexed into a shared pipeline can be retrieved by anyone asking the central Joule directly, regardless of which capability they used. For general product guidelines this is usually acceptable. For sensitive content, such as internal pricing policies, salary bands, or legal documents, you need data isolation at the storage layer, not just at the capability level.The next blog post,  Building Custom Joule Capabilities: Private Document Grounding with SAP AI Core , addresses this directly. It moves the grounding pipeline to your own SAP AI Core instance so that documents are only reachable through your capability, with no path through central Joule.”}]] Read More Technology Blog Posts by SAP articles 

#SAPCHANNEL

By ali

Leave a Reply