Orchestrate

Joule and Joule Studio

What Joule does for users, how Joule skills and Joule agents differ, what changed with the new Joule Studio in 2026, and how to tell which one a request needs.

Updated Oct 6, 2026Foundational 9 minDeep 40 min
Foundational layer · 9 min read

The 60-second version

Joule is SAP's AI assistant. People type a request inside SAP cloud applications, and Joule answers, takes them to the right screen, or does a task for them.

What Joule can do comes from a catalog of two kinds of building blocks:

  • A Joule skill does one fixed job. "Show sales order 4711" is a skill: it reads the order and shows it, the same way every time.
  • A Joule agent works toward a goal. "Why is order 4711 blocked and what should happen next?" needs an agent: it decides which steps to take, often by calling several skills in turn.

SAP ships many skills and agents. Joule Studio is where your team builds its own. As of October 2026 there are two generations of it: the Joule Studio inside SAP Build, described in SAP's April 2026 architecture guidance, and a new, fully managed Joule Studio that SAP announced in May 2026 and is still rolling out.

The decision for you: when someone says "let's do this with Joule", ask whether it is a fixed job (a skill), a goal with judgement in it (an agent), or something SAP already ships.

Why it matters to the business

Take the running example from this course: sales orders blocked by the credit check in order-to-cash. A clerk today opens the order, looks up the customer's exposure, checks the credit policy and files a review request. That is four screens and some judgement for every blocked order.

Joule changes where that work happens, in three steps of ambition:

Request What answers it Business effect
"Show me order 4711" A skill Saves a few clicks; low risk
"Take me to order 4711" A navigational skill Helps occasional users find the right app
"Why is 4711 blocked and what next?" An agent using several skills Saves minutes per order; needs controls

The value grows down the table, and so does the risk. A skill that reads an order can show wrong data at worst. An agent that files requests or changes data can do the wrong thing, many times, quickly.

Cost follows the same shape. SAP's commercial model splits Joule into Joule Base, which comes with your SAP cloud subscription, and Joule Premium, paid for in AI Units. SAP Learning's Joule course places navigational, informational, transactional and simple analytical capabilities in Base, and "agent actions executed by Joule Agents" in Premium. So moving from skills to agents is also a move from included to metered.

How SAP does it

As of 6 October 2026, from SAP's own pages:

  • Joule runs as one central instance on SAP BTP, shared across SAP cloud applications. SAP Learning says all applications in that setup must use the same SAP Cloud Identity Services tenant, so Joule knows who the user is everywhere.
  • Joule Studio in SAP Build lets teams build skills and agents with low-code tools. SAP's Architecture Center describes skills that call OData APIs through destinations, and agents that run on SAP AI Core. Deploying registers them in Joule's catalog.
  • The new Joule Studio, announced on 13 May 2026, is a fully managed service for agents, apps and workflows. It adds pro-code development with frameworks such as LangChain, Pydantic AI and LlamaIndex, an embedded n8n environment, and a managed Joule Studio runtime.
  • Status. SAP's Sapphire 2026 guide said the new Joule Studio was in an early customer adoption program with general availability "expected in Q3 2026". We did not find a general availability announcement in the pages we opened. SAP's August 2026 reference architecture still says some capabilities are "not yet generally available".
  • Free periods. SAP offers free design-time access for customers and partners through the end of 2026, under fair-use limits. Its September 2026 announcement says the runtime is free "through October 2026". Both are for customers and partners, and the runtime window ends this month.

Skill, agent or standard: a decision guide

Work down this list for each request your business wants Joule to handle.

Question If yes Example from order-to-cash
Does SAP already ship a skill or agent for it in our applications? Turn it on and measure adoption Ask your account team what exists for your release
Is it one fixed job with clear inputs and one system call? Build a skill Show an order, list blocked orders, open an app
Does it need several steps, and the steps depend on what it finds? Build an agent that uses skills Explain a block and propose the right reviewer
Does it change data or start a process? Add a confirmation step, whatever you build File a credit review request
Is the logic large, custom, or owned by a pro-code team? Consider pro-code, connected to Joule An agent that already exists in Python

Two rules sit across every row. First, Joule has to pick your skill or agent for the right requests, so test that with real requests from real users before go-live. Second, SAP's own guidance says to "always implement confirmation dialogs" for create, update and delete operations.

Questions to ask

  • Which Joule capabilities are in Joule Base for our applications, and which consume AI Units? What would a year cost at our volumes?
  • Is our Joule set up as one instance across our SAP cloud applications, with one SAP Cloud Identity Services tenant?
  • Which Joule Studio do we have access to: the one in SAP Build, the new managed one, or both? What happens to our free access after October and after December 2026?
  • For each custom skill or agent: what does it change, who confirms it, and how do we test that Joule picks it for the right requests?
  • Who owns the catalog of custom skills and agents, and who retires them?
  • If we already have a pro-code agent, do we rebuild it in Joule Studio or connect it to Joule?

Common misconceptions

  • "Joule is a chatbot on top of SAP." Joule is an entry point to a catalog of skills and agents. What it can do depends on what is in that catalog for your applications.
  • "Everything should be an agent." A fixed job is cheaper, more predictable and easier to test as a skill. Agents earn their place when the steps depend on what they find.
  • "Joule Studio is one product with one status." As of October 2026 there is the Joule Studio in SAP Build and a new, managed Joule Studio still moving toward general availability. Ask which one a proposal means.
  • "A confirmation step is the security control." Confirmation stops accidents. Who may do what is still decided by the user's identity and SAP authorizations.
  • "Free Joule Studio means free agents in production." The free offers SAP announced are time-limited and for customers and partners. Plan the running cost separately.

Key terms

  • Joule: SAP's AI assistant inside SAP cloud applications.
  • Joule skill: a fixed, rule-based job Joule can run, such as reading one record.
  • Joule agent: a component that plans several steps toward a goal, usually by calling skills and other tools.
  • Joule Studio: SAP's environment for building custom skills and agents (and, in the new version, apps and workflows).
  • Joule Studio runtime: SAP's managed service that runs what you build in the new Joule Studio.
  • Joule Base / Joule Premium: included capabilities versus advanced capabilities and agent actions paid in AI Units.
  • AI Units: SAP's prepaid currency for premium AI services, bought annually.

Check yourself

Pick one answer for each question. The explanation appears after you choose.
  1. 1A sales lead wants Joule to "show the open items for customer 10023". What should the team build, if SAP doesn't ship it?

    Answer: B. Reading one customer's open items is a fixed job with a clear input and one system call, which is what a skill is for. An agent would add planning, cost and risk without adding value here.
  2. 2When does an agent earn its place over a skill?

    Answer: C. An agent plans and chooses its next step from what it has learned, such as checking credit only when the block is a credit block. If the steps are always the same, a skill does the job more cheaply and predictably.
  3. 3Your CFO asks why moving from skills to agents changes the budget. What is the accurate answer?

    Answer: D. SAP's commercial model puts navigational, informational, transactional and simple analytical capabilities in Joule Base. Agent actions by Joule Agents are Joule Premium and draw on AI Units, which are bought annually.
  4. 4A partner proposes a demo "in Joule Studio" next month. What should you ask first?

    Answer: A. As of October 2026 there is the Joule Studio in SAP Build and a new, managed Joule Studio that SAP said was in early adoption with GA expected in Q3 2026. Free runtime access ends in October 2026, so access and terms decide what can be demonstrated.
  5. 5A custom skill files credit review requests. What does SAP's guidance say it must include?

    Answer: C. SAP's Architecture Center says to always implement confirmation dialogs for create, update and delete operations. A confirmation prevents accidents; the user's authorizations still decide what they may do.
  6. 6Why does SAP ask all applications in a unified Joule setup to share one SAP Cloud Identity Services tenant?

    Answer: B. One identity tenant gives each user one consistent identity across the applications Joule serves. That is what lets skills and agents act as that user rather than as an all-powerful technical account.
  7. 7Someone says "we got Joule Studio free, so our agents will run free". What is wrong?

    Answer: D. SAP announced free design-time access through the end of 2026 under fair-use limits, and a free runtime through October 2026. Production cost after those windows needs its own answer from your account team.
Deep layer · 40 min read

Mental model

Joule is a router in front of a catalog. A request comes in; Joule decides which entry in the catalog should handle it; that entry does its work as the signed-in user; Joule shows the result.

Everything Joule Studio does is about adding entries to that catalog:

  • A skill is an entry with a fixed recipe: a purpose, parameters, one or more actions, and a reply.
  • An agent is an entry with a goal, instructions and tools. Its tools are usually skills, plus retrieval over documents (RAG) and, in SAP's newer architecture, tools reached over MCP.

So two things decide whether your custom work succeeds: whether Joule picks your entry for the right requests, and whether the entry does the right thing once picked. You test both separately. That is exactly what the lab in this topic builds on your laptop.

How it works

The path of one request

sequenceDiagram
  participant U as User in SAP app
  participant J as Joule
  participant C as Catalog
  participant S as Skill or agent
  participant B as SAP backend
  U->>J: "Why is 4711 blocked?"
  J->>C: match request to an entry
  C-->>J: agent: order exceptions
  J->>S: run with user's identity
  S->>B: read order (OData)
  B-->>S: order data
  S-->>J: answer, or ask to confirm
  J-->>U: reply

What each part does, as SAP describes it:

  1. One Joule, many applications. SAP Learning describes a unified Joule instance on SAP BTP, with dev, test and prod, shared across SAP cloud applications. All of them must use the same SAP Cloud Identity Services tenant.
  2. The catalog. SAP's golden path for agents says deploying from Joule Studio "creates all necessary Joule artifacts (scenarios, dialog functions)" and registers the agent in Joule's catalog. Those are the entries Joule chooses from. SAP does not describe the matching logic in the pages we opened, so don't build assumptions on it; test it.
  3. The entry runs. A skill calls APIs through actions and destinations. A low-code agent runs on SAP AI Core and calls its tools.
  4. Identity. The new architecture adds an Agent Gateway, which SAP's August 2026 reference architecture describes as handling "authentication, principal propagation, policy enforcement and tenancy" for agents.

Inside a skill

SAP's Architecture Center lists the build steps for a skill in Joule Studio in SAP Build:

  1. Create a project in Joule Studio from the SAP Build lobby.
  2. Design the skill logic by defining parameters and actions.
  3. Configure destinations, using action projects that wrap OData APIs.
  4. Map data with the skill editor and the Formula Editor.
  5. Test in the standalone Joule assistant.

It names the kinds of skill as navigation, list and search, transactions (create, update, delete), decision-making and data access. It describes two places to deploy: a standalone environment for isolated testing and a shared environment where users of SAP Build Work Zone, SAP S/4HANA and SAP SuccessFactors reach it. And it gives one hard rule: confirmation dialogs for every transactional operation.

Inside an agent

SAP Learning puts the difference this way: skills handle "simpler, rule-based tasks", agents handle goal-oriented scenarios "that require dynamic planning and reasoning". The golden path describes low-code agents with "planning, RAG (Retrieval-Augmented Generation) and tool chaining", running on SAP AI Core "with built-in metering, tracing and security".

You have built this loop already in Agents from first principles and Multi-step agents. Joule Studio's agent builder gives you the same loop as configuration: instructions and tools, run on SAP AI Core.

Two generations of Joule Studio

As of 6 October 2026, you will meet both in SAP material:

Joule Studio in SAP Build New Joule Studio (announced May 2026)
What you build Skills and agents Agents, apps and workflows
How you build Low-code builders in SAP Build Browser-based low-code builder, or your own IDE with the Joule Studio CLI and a coding agent over MCP
Pro-code frameworks Pro-code agents connect separately (Bring Your Own Agent, A2A) LangChain, Pydantic AI and LlamaIndex named by SAP; embedded n8n
Where it runs Agents on SAP AI Core SAP-managed Joule Studio runtime
Status Documented by SAP's golden path (April 2026) Early adoption; GA "expected in Q3 2026" per Sapphire guide; not confirmed in pages we opened

The August 2026 reference architecture says the low-code and pro-code flows in the new Joule Studio "produce the same deployable artifact, automatically registered with Joule". It also describes intent-based development: a six-phase flow from intent through requirements, specification, code generation and testing to deployment.

Build it yourself: a Joule-shaped assistant

You will build a small assistant on your laptop that works the way the mental model says: a catalog of six skills, a router that picks one skill per request, and an agent that chains skills toward a goal. One skill changes something, so it stops and asks before it runs. Then you measure how often the router picks the right skill.

This is a teaching model, not SAP's code. Joule's real matching uses SAP's own logic, which SAP doesn't describe in the pages we opened. The stand-in router here matches words in each skill's description, which makes one thing very visible: in this router, the description is what gets a skill picked, and you can measure how well.

Before you start: complete Set up your computer for this course and Set up for Unit 9, which creates the orchestrate-course folder, the .venv virtual environment and the unit09 subfolder. This walkthrough doesn't repeat those steps.

flowchart LR
  R[Request] --> RT[Router]
  RT -->|one skill| SK[Skill]
  R2[Goal] --> AG[Agent]
  AG -->|several skills| SK
  SK --> D[Made-up SAP data]
  SK -->|changes data?| CF[Confirm first]

What you need

  • Your course folder with .venv, from Unit 1 and Set up for Unit 9.
  • About 45 to 60 minutes.
  • No account and no cost. The lab uses only Python's built-in modules and made-up data.

Step 1: Open your course folder and turn on the virtual environment

  1. Open VS Code, choose File > Open Folder, and open orchestrate-course.

  2. Open a terminal: Terminal > New Terminal.

  3. If the prompt doesn't start with (.venv), turn it on:

    • Windows (PowerShell):

      .venv\Scripts\Activate.ps1
    • macOS / Linux:

      source .venv/bin/activate

Run every command in this topic from the course folder, not from inside unit09. Nothing needs installing: requirements.txt doesn't change.

Step 2: Save the lab file

  1. In VS Code, right-click unit09, choose New File, name it joule_lab.py, paste the code below and save.
"""Unit 9: a Joule-shaped assistant on your laptop.

Skills are small, fixed jobs with a description, parameters, one action and a reply.
A router picks one skill per request. An agent chains several skills toward a goal.
Transactional skills never run without an explicit confirmation.

This is a teaching model of the ideas, not SAP's implementation. All data is made up.
Nothing here changes SAP data: the one "write" appends a line to a local file.

  python unit09/joule_lab.py ask "show me sales order 4711"
  python unit09/joule_lab.py ask "request a credit review for order 4711" --confirm yes
  python unit09/joule_lab.py agent "why is order 4711 blocked and what should happen next"
  python unit09/joule_lab.py eval
"""
import argparse
import json
import re
import sys
import time
from pathlib import Path

HERE = Path(__file__).resolve().parent
REQUESTS = HERE / "joule_requests.jsonl"

# ---------------------------------------------------------------- made-up, SAP-shaped data
# Field names match the sales order examples in earlier units; "block_note" is made up.
ORDERS = {
    "4711": {"SalesOrder": "4711", "SoldToParty": "10023", "TotalNetAmount": "1800.00",
             "TransactionCurrency": "EUR", "block_note": "Blocked by the credit check."},
    "4723": {"SalesOrder": "4723", "SoldToParty": "10051", "TotalNetAmount": "640.00",
             "TransactionCurrency": "EUR", "block_note": "Incomplete: delivery address data missing."},
    "4725": {"SalesOrder": "4725", "SoldToParty": "10077", "TotalNetAmount": "3900.00",
             "TransactionCurrency": "EUR", "block_note": "Pricing: customer disputes the price."},
    "4730": {"SalesOrder": "4730", "SoldToParty": "10023", "TotalNetAmount": "250.00",
             "TransactionCurrency": "EUR", "block_note": ""},
}
CREDIT = {
    "10023": {"credit_limit": 50000.0, "open_items": 50700.0, "currency": "EUR"},
    "10051": {"credit_limit": 30000.0, "open_items": 4100.0, "currency": "EUR"},
    "10077": {"credit_limit": 80000.0, "open_items": 12000.0, "currency": "EUR"},
}
POLICY = [
    "Credit blocks up to 5 percent over the limit are decided by the credit manager.",
    "Credit blocks more than 5 percent over the limit are decided by the head of finance.",
    "Incomplete orders are fixed by completing master data, never by a credit review.",
]


# ---------------------------------------------------------------- the actions skills call
def act_show_order(p):
    order = ORDERS.get(p["order"])
    if not order:
        return {"error": f"sales order {p['order']} not found"}
    return order


def act_list_blocked(p):
    return {"blocked": [o["SalesOrder"] for o in ORDERS.values() if o["block_note"]]}


def act_credit(p):
    credit = CREDIT.get(p["customer"])
    if not credit:
        return {"error": f"customer {p['customer']} not found"}
    return {"customer": p["customer"], **credit}


def act_open_app(p):
    # Made-up link. A real navigational skill would return a link into your launchpad.
    return {"link": f"https://launchpad.example.com/#orders/{p['order']}"}


def act_policy(p):
    # Keep only the passages that share the most words with the question.
    words = set(tokens(p["question"]))
    scored = [(len(words & set(tokens(line))), line) for line in POLICY]
    best = max(score for score, _ in scored)
    hits = [line for score, line in scored if score == best and score > 1]
    return {"passages": hits or ["No policy passage matched."]}


def act_request_review(p):
    if p["order"] not in ORDERS:
        return {"error": f"sales order {p['order']} not found"}
    request_id = f"CR-{p['order']}"           # one per order: a retry files nothing new
    existing = REQUESTS.read_text(encoding="utf-8") if REQUESTS.exists() else ""
    if f'"request_id": "{request_id}"' in existing:
        return {"request_id": request_id, "status": "already filed"}
    with open(REQUESTS, "a", encoding="utf-8") as handle:
        handle.write(json.dumps({"request_id": request_id, "order": p["order"],
                                 "filed_at": time.strftime("%Y-%m-%dT%H:%M:%S")}) + "\n")
    return {"request_id": request_id, "status": "filed"}


# ---------------------------------------------------------------- the skill catalog
# Each skill: what it is for (description), what it needs (parameters), what it does
# (action), what the user sees (reply), and whether it changes something (confirm).
SKILLS = {
    "show_sales_order": {
        "kind": "informational",
        "description": "Show one sales order: customer, net value, currency and block reason.",
        "parameters": ["order"],
        "action": act_show_order,
        "reply": lambda r: (f"Order {r['SalesOrder']}: customer {r['SoldToParty']}, "
                            f"{r['TotalNetAmount']} {r['TransactionCurrency']}. "
                            f"Block: {r['block_note'] or 'none'}"),
        "confirm": False,
    },
    "list_blocked_orders": {
        "kind": "informational",
        "description": "List every order that is blocked right now.",
        "parameters": [],
        "action": act_list_blocked,
        "reply": lambda r: "Blocked orders: " + ", ".join(r["blocked"]),
        "confirm": False,
    },
    "show_credit_exposure": {
        "kind": "informational",
        "description": "Show a customer's credit limit and open items.",
        "parameters": ["customer"],
        "action": act_credit,
        "reply": lambda r: (f"Customer {r['customer']}: limit {r['credit_limit']:,.0f}, "
                            f"open items {r['open_items']:,.0f} {r['currency']}"),
        "confirm": False,
    },
    "open_sales_order_app": {
        "kind": "navigational",
        "description": "Open the app screen so I can work on an order myself.",
        "parameters": ["order"],
        "action": act_open_app,
        "reply": lambda r: f"Open this link: {r['link']}",
        "confirm": False,
    },
    "credit_policy": {
        "kind": "informational",
        "description": "Answer questions about the credit policy: who decides, rules, limits.",
        "parameters": ["question"],
        "action": act_policy,
        "reply": lambda r: " ".join(r["passages"]),
        "confirm": False,
    },
    "request_credit_review": {
        "kind": "transactional",
        "description": "Request a credit review for a blocked order.",
        "parameters": ["order"],
        "action": act_request_review,
        "reply": lambda r: f"Credit review {r['request_id']}: {r['status']}.",
        "confirm": True,
    },
}

STOP = {"a", "an", "the", "for", "of", "to", "me", "my", "i", "is", "and", "so", "can", "on",
        "it", "what", "who", "please", "show", "all", "this", "that", "with", "do", "does", "be"}


def tokens(text):
    """Lowercase words without stop words, with a crude plural strip ('orders' -> 'order')."""
    words = re.findall(r"[a-z]+", text.lower())
    return [w[:-1] if w.endswith("s") and len(w) > 3 else w for w in words if w not in STOP]


def route(utterance):
    """Pick the skill whose description shares the most words with the request.

    A stand-in for the model-based selection a real assistant uses. Returns (skill, score),
    or (None, 0) when nothing matches or two skills tie: then the assistant should say so.
    """
    asked = set(tokens(utterance))
    scores = sorted(((len(asked & set(tokens(s["description"]))), name)
                     for name, s in SKILLS.items()), reverse=True)
    best, runner_up = scores[0], scores[1]
    if best[0] == 0 or best[0] == runner_up[0]:
        return None, best[0]
    return best[1], best[0]


def extract(skill, utterance):
    """Fill parameters from the text: 4-digit order numbers, 5-digit customer numbers."""
    found = {}
    if "order" in skill["parameters"]:
        m = re.search(r"\b(\d{4})\b", utterance)
        if m:
            found["order"] = m.group(1)
    if "customer" in skill["parameters"]:
        m = re.search(r"\b(\d{5})\b", utterance)
        if m:
            found["customer"] = m.group(1)
    if "question" in skill["parameters"]:
        found["question"] = utterance
    return found


def run_skill(name, params, confirm):
    """Run one skill. Transactional skills stop and ask unless confirm is 'yes'."""
    skill = SKILLS[name]
    missing = [p for p in skill["parameters"] if p not in params]
    if missing:
        return {"status": "need_input", "text": f"Which {missing[0]}? ({name} needs it.)"}
    if skill["confirm"] and confirm != "yes":
        if confirm == "no":
            return {"status": "cancelled", "text": "Cancelled. Nothing was changed."}
        return {"status": "confirm",
                "text": f"I am about to run {name} with {params}. Run again with --confirm yes "
                        "to go ahead, or --confirm no to cancel."}
    result = skill["action"](params)
    if "error" in result:
        return {"status": "error", "text": result["error"]}
    return {"status": "done", "text": skill["reply"](result), "result": result}


def ask(utterance, confirm=None):
    name, score = route(utterance)
    if not name:
        return name, {"status": "no_skill",
                      "text": "I don't have a skill for that. Try rephrasing, or ask about an order."}
    return name, run_skill(name, extract(SKILLS[name], utterance), confirm)


def agent(goal, confirm=None):
    """Chain skills toward a goal. A fixed plan stands in for a model's planning step."""
    trace = []
    m = re.search(r"\b(\d{4})\b", goal)
    if not m:
        return trace, "Which order should I look at?"
    order = m.group(1)

    def step(name, params, conf=None):
        out = run_skill(name, params, conf)
        trace.append({"skill": name, "params": params, "status": out["status"]})
        return out

    header = step("show_sales_order", {"order": order})
    if header["status"] != "done":
        return trace, header["text"]
    note = header["result"]["block_note"].lower()
    if not note:
        return trace, f"Order {order} is not blocked. Nothing to do."
    if not note.startswith("blocked by the credit"):
        return trace, f"Order {order}: {header['result']['block_note']} This is not a credit block, so no credit review."
    customer = header["result"]["SoldToParty"]
    credit = step("show_credit_exposure", {"customer": customer})["result"]
    exposure = credit["open_items"] + float(header["result"]["TotalNetAmount"])
    over = (exposure / credit["credit_limit"] - 1) * 100
    step("credit_policy", {"question": "who decides a credit block over the limit"})
    decider = "the credit manager" if round(over, 2) <= 5 else "the head of finance"
    review = step("request_credit_review", {"order": order}, confirm)
    summary = (f"Order {order} is credit-blocked: exposure {exposure:,.0f} {credit['currency']} is "
               f"{over:.1f}% over the {credit['credit_limit']:,.0f} limit, so {decider} decides "
               f"under the credit policy.\nNext: {review['text']}")
    return trace, summary


# ---------------------------------------------------------------- evaluation
EVAL = [
    ("show me sales order 4711", "show_sales_order"),
    ("what is the block reason on order 4723", "show_sales_order"),
    ("list blocked sales orders", "list_blocked_orders"),
    ("which orders are blocked right now", "list_blocked_orders"),
    ("credit limit for customer 10023", "show_credit_exposure"),
    ("how much does customer 10051 owe in open items", "show_credit_exposure"),
    ("open order 4725 in the app", "open_sales_order_app"),
    ("take me to the screen for 4730 so I can fix it myself", "open_sales_order_app"),
    ("who decides on a credit block over the limit", "credit_policy"),
    ("what are the policy rules for incomplete orders", "credit_policy"),
    ("request a credit review for order 4711", "request_credit_review"),
    ("please get finance to look at the credit block on 4711", "request_credit_review"),
    ("book a meeting room for tomorrow", None),
]


def evaluate(out=None):
    rows = []
    for utterance, expected in EVAL:
        got, score = route(utterance)
        rows.append({"request": utterance, "expected": expected, "got": got, "score": score,
                     "ok": got == expected})
        print(f"{'OK  ' if got == expected else 'MISS'} {utterance!r:60} expected={expected} got={got}")
    right = sum(r["ok"] for r in rows)
    print(f"\nRouting accuracy: {right}/{len(rows)} = {right / len(rows):.0%}")
    if out:
        report = {"run_at": time.strftime("%Y-%m-%dT%H:%M:%S"), "skills": len(SKILLS),
                  "correct": right, "total": len(rows), "rows": rows}
        Path(out).write_text(json.dumps(report, indent=2), encoding="utf-8")
        print(f"Report saved to {out}")


def main():
    parser = argparse.ArgumentParser(description="A Joule-shaped assistant on made-up SAP data.")
    sub = parser.add_subparsers(dest="cmd", required=True)
    p_ask = sub.add_parser("ask", help="route one request to one skill")
    p_ask.add_argument("text")
    p_ask.add_argument("--confirm", choices=["yes", "no"])
    p_agent = sub.add_parser("agent", help="chain several skills toward a goal")
    p_agent.add_argument("text")
    p_agent.add_argument("--confirm", choices=["yes", "no"])
    p_eval = sub.add_parser("eval", help="measure the router on a fixed set of requests")
    p_eval.add_argument("--out", help="also save the results as a JSON report")
    sub.add_parser("skills", help="print the skill catalog")
    args = parser.parse_args()

    if args.cmd == "ask":
        name, out = ask(args.text, args.confirm)
        print(f"skill:  {name}\nstatus: {out['status']}\nreply:  {out['text']}")
    elif args.cmd == "agent":
        trace, summary = agent(args.text, args.confirm)
        for i, t in enumerate(trace, 1):
            print(f"step {i}: {t['skill']} {t['params']} -> {t['status']}")
        print(f"\n{summary}")
    elif args.cmd == "eval":
        evaluate(args.out)
    else:
        for name, s in SKILLS.items():
            flag = " (asks for confirmation)" if s["confirm"] else ""
            print(f"{name:22} {s['kind']:14} {s['description']}{flag}")


if __name__ == "__main__":
    sys.exit(main())

Step 3: Look at the catalog

  1. Print the six skills:

    python unit09/joule_lab.py skills

What success looks like:

show_sales_order       informational  Show one sales order: customer, net value, currency and block reason.
list_blocked_orders    informational  List every order that is blocked right now.
show_credit_exposure   informational  Show a customer's credit limit and open items.
open_sales_order_app   navigational   Open the app screen so I can work on an order myself.
credit_policy          informational  Answer questions about the credit policy: who decides, rules, limits.
request_credit_review  transactional  Request a credit review for a blocked order. (asks for confirmation)

The kinds mirror the ones SAP names for Joule: navigational, informational and transactional. Only the transactional one asks for confirmation.

Step 4: Ask for one thing at a time

  1. Ask for an order:

    python unit09/joule_lab.py ask "show me sales order 4711"
    skill:  show_sales_order
    status: done
    reply:  Order 4711: customer 10023, 1800.00 EUR. Block: Blocked by the credit check.
  2. Ask without the order number:

    python unit09/joule_lab.py ask "show me sales order"
    skill:  show_sales_order
    status: need_input
    reply:  Which order? (show_sales_order needs it.)

    The router found the skill, but a parameter is missing, so it asks instead of guessing.

  3. Ask for something no skill covers:

    python unit09/joule_lab.py ask "book a meeting room for tomorrow"
    skill:  None
    status: no_skill
    reply:  I don't have a skill for that. Try rephrasing, or ask about an order.

    This "empty" result is correct behaviour. An assistant that always picks something will run the wrong skill.

  4. Ask the policy question:

    python unit09/joule_lab.py ask "who decides on a credit block over the limit"
    skill:  credit_policy
    status: done
    reply:  Credit blocks up to 5 percent over the limit are decided by the credit manager. Credit blocks more than 5 percent over the limit are decided by the head of finance.

Step 5: Try the transactional skill

  1. Ask for a credit review:

    python unit09/joule_lab.py ask "request a credit review for order 4711"
    skill:  request_credit_review
    status: confirm
    reply:  I am about to run request_credit_review with {'order': '4711'}. Run again with --confirm yes to go ahead, or --confirm no to cancel.
  2. Confirm it:

    python unit09/joule_lab.py ask "request a credit review for order 4711" --confirm yes
    skill:  request_credit_review
    status: done
    reply:  Credit review CR-4711: filed.
  3. Run the same command again. The reply now ends already filed: the action uses one request ID per order, so a repeated confirmation files nothing new.

  4. Open unit09/joule_requests.jsonl to see the one line that was written. That file stands in for a workflow inbox. Nothing in SAP changed.

Step 6: Give the agent a goal

  1. Delete the request file so you start clean:

    • Windows (PowerShell):

      Remove-Item unit09\joule_requests.jsonl
    • macOS / Linux:

      rm unit09/joule_requests.jsonl
  2. Give the agent a goal:

    python unit09/joule_lab.py agent "why is order 4711 blocked and what should happen next"

What success looks like:

step 1: show_sales_order {'order': '4711'} -> done
step 2: show_credit_exposure {'customer': '10023'} -> done
step 3: credit_policy {'question': 'who decides a credit block over the limit'} -> done
step 4: request_credit_review {'order': '4711'} -> confirm

Order 4711 is credit-blocked: exposure 52,500 EUR is 5.0% over the 50,000 limit, so the credit manager decides under the credit policy.
Next: I am about to run request_credit_review with {'order': '4711'}. Run again with --confirm yes to go ahead, or --confirm no to cancel.

The agent called four skills, chose the second one only because the first showed a credit block, and stopped before the one that writes.

  1. Try order 4723 instead. The agent stops after one step: the block is missing address data, so a credit review would be the wrong action.

    python unit09/joule_lab.py agent "why is order 4723 blocked"
    step 1: show_sales_order {'order': '4723'} -> done
    
    Order 4723: Incomplete: delivery address data missing. This is not a credit block, so no credit review.

Step 7: Measure the router

  1. Run the evaluation set and save a report:

    python unit09/joule_lab.py eval --out unit09/joule_eval.json

What success looks like (the request column is shortened here):

OK   'show me sales order 4711'                                   expected=show_sales_order got=show_sales_order
...
MISS 'please get finance to look at the credit block on 4711'     expected=request_credit_review got=None
OK   'book a meeting room for tomorrow'                           expected=None got=None

Routing accuracy: 12/13 = 92%
Report saved to unit09/joule_eval.json

The one miss is a paraphrase: the request shares only "credit" with the right description, which ties with the policy skill, so the router declines. A model-based router handles paraphrases better than word matching, but the lesson holds: you only know how often Joule picks your skill if you keep a list of real requests and test it.

Step 8: Save your work in Git

  1. Check what Git sees:

    git status

    You should see unit09/joule_lab.py, unit09/joule_eval.json and possibly unit09/joule_requests.jsonl.

  2. Save the code and the report, not the request file:

    git add unit09/joule_lab.py unit09/joule_eval.json
    git commit -m "Unit 9: Joule-shaped skills, router, agent and routing eval"

How the code works

Part What it does
ORDERS, CREDIT, POLICY Made-up data, with order field names matching earlier units' sales order examples
act_* functions The actions: the one thing each skill does. Only act_request_review writes, and only to a local file
SKILLS The catalog. Each entry has a kind, a description, parameters, an action, a reply and a confirm flag
tokens Turns text into lowercase words without filler words, so descriptions and requests can be compared
route Scores each description by shared words; declines on no match or a tie
extract Fills parameters: four digits are an order, five digits a customer
run_skill Asks for missing parameters, stops transactional skills until --confirm yes, then runs the action
agent Chains skills toward a goal and decides the next step from what the last one returned
EVAL / evaluate The evaluation set and the routing score, optionally saved as JSON

If something goes wrong

What you see What it means What to do
python is not recognized, or command not found Python isn't installed or isn't on the path Windows: repeat Unit 1, Step 1, then open a new terminal. macOS/Linux: use python3 until .venv is active
can't open file ... joule_lab.py The file isn't saved where the command looks Save it as unit09/joule_lab.py and run from the course folder, not from inside unit09
error: the following arguments are required: text You left out the request Put the request in quotes after ask or agent
SyntaxError or IndentationError The paste lost or added spaces Paste the whole file again into an empty file
PowerShell: running scripts is disabled when activating .venv Windows blocks the activation script Run Set-ExecutionPolicy -Scope CurrentUser RemoteSigned, then try again
Remove-Item or rm says the file doesn't exist No request was filed yet Nothing to do; carry on
The reply says already filed An earlier run filed that request Expected; delete unit09/joule_requests.jsonl to start clean
A company proxy or blocked network Not relevant here The lab makes no network calls, so it runs offline

The SAP way

As of 6 October 2026, here is how the same ideas look in SAP's products, from the sources opened in this run.

Building a skill in Joule Studio in SAP Build

The lab's SKILLS entries map onto what SAP's golden path describes:

Lab Joule Studio in SAP Build
description The skill's name and stated purpose (how Joule uses it for matching isn't described in the pages we opened)
parameters Skill parameters
act_* function Actions; an action project wraps an OData API, reached through a destination
reply The response, with data mapped in the skill editor and Formula Editor
confirm: True A confirmation dialog, which SAP says to always add for create, update and delete
Running ask locally Testing in the standalone Joule assistant before the shared environment

The golden path also says skills can start SAP Build Process Automation workflows. That is how a request such as the lab's credit review would usually reach a person in a real system: as a workflow task, not a direct write.

Building an agent

  • Low-code, Joule Studio in SAP Build. You define the agent's instructions and configure its tools. The golden path says these agents run on SAP AI Core, and that deploying creates the Joule scenarios and dialog functions and registers the agent in Joule's catalog.
  • Pro-code, connected to Joule. An agent you write yourself, for example with the frameworks from Agent frameworks compared, connects through SAP's Bring Your Own Agent pattern by exposing an A2A server endpoint.
  • The new Joule Studio. SAP's August 2026 reference architecture describes a browser builder and a pro-code flow with the Joule Studio CLI and a coding agent connected over MCP, deploying to the SAP-managed runtime. SAP's May 2026 announcement names LangChain, Pydantic AI and LlamaIndex for pro-code work.

The runtime and its controls

SAP's 28 September 2026 announcement says the Joule Studio runtime "helps determine whether an action should execute at all", by applying business authorization, role-based policy and process context. The runtime uses NVIDIA OpenShell for isolation. SAP's May 2026 announcement adds persistent memory in SAP HANA Cloud.

What is GA and what isn't

  • The golden path pages for skills and agents in SAP Build were updated on 23 April 2026 and describe them without a preview label.
  • The new Joule Studio: early adoption, with general availability "expected in Q3 2026" according to the Sapphire 2026 guide. The guide also lists the Joule Studio runtime as planned for GA in Q3 2026. We found no GA announcement in the pages opened on 6 October 2026.
  • Third-party agents calling Joule Agents over A2A in both directions: planned for Q4 2026 per the Sapphire guide.
  • SAP's August 2026 reference architecture: "some components and capabilities of this reference architecture are not yet generally available".

Licensing

  • Joule Base is included in standard cloud subscriptions, per SAP's pricing page.
  • Joule Premium capabilities, including agent actions, consume AI Units. SAP's pricing page says AI Units are bought annually and expire after 12 months, and Premium packages range from 8 to 1 AI Units per user per month by volume. SAP Learning adds overages at 2 AI Units per 1,000 requests.
  • Joule Studio is accessed through SAP BTP credits, per the pricing page. Free design-time access runs through the end of 2026 and the free runtime through October 2026, for customers and partners.

Prices and terms change. Confirm them with your account team before a business case.

Build vs. SAP

Situation Better choice Why
Users already work in SAP cloud applications and want answers there A Joule skill or agent It reaches users where they work, under their own identity
One fixed job on a released OData API A skill in Joule Studio Low-code, deterministic, cheap to test
A goal with judgement, using a few SAP reads A low-code Joule agent Planning and tool use as configuration, run on SAP AI Core
Complex logic, your own tests, an existing Python agent Pro-code, connected to Joule You keep the code; Joule gets an entry through A2A
A prototype before you have any SAP access Build it yourself, like this lab Free, and the skills and eval set carry over
Access to the new Joule Studio isn't confirmed Don't plan the project around it Status and free terms are still moving in October 2026

Production concerns

  • Identity and SAP authorizations. A skill or agent must act as the signed-in user, so SAP's own authorization checks apply. SAP's unified Joule setup needs one SAP Cloud Identity Services tenant; the new architecture adds the Agent Gateway for principal propagation and policy. Don't let a destination use a technical user with broad rights "to make it work". See Grounding on SAP data with authorizations.
  • Confirmation is not authorization. The lab's --confirm yes stops accidents. It doesn't check who confirmed. In SAP, the user's roles decide what the confirmed action may do.
  • Evaluation. Keep an evaluation set per skill: real requests that should reach it and near misses that shouldn't. Re-run it whenever you add or reword a skill, because a new description can steal requests from an old one.
  • Catalog hygiene. Every custom entry competes for requests. Give each an owner, a description reviewed like an API contract, and a retirement date.
  • Landscapes. SAP Learning describes dev, test and prod for the unified Joule instance. Test new skills against non-production systems first, as SAP's standalone and shared environments allow.
  • Cost. Agent actions consume AI Units; skills mostly don't. Count requests in testing and estimate a year at production volume before you choose an agent over a skill.
  • Clean core. Skills reach SAP through APIs and destinations, and agents through skills and tools. Neither needs a modification in the core. Prefer released APIs, as in SAP tools for agents.

Pitfalls

  • Vague descriptions. In the lab, "Handles orders" ties with every other order skill. In any catalog, say what the skill does, on what, and for whom.
  • Agents for fixed jobs. If the steps never change, an agent adds cost and variance for nothing.
  • Writing without confirmation. SAP's guidance is explicit, and the lab shows how little code it takes.
  • Always answering. An assistant that never says "I can't do that" will run the wrong skill. Test the empty case.
  • Planning on access you don't have. The new Joule Studio's status and free terms are moving. Check before you promise a demo.
  • Treating the lab's router as Joule. The lab shows why descriptions matter. Joule's own matching is SAP's, and you test it through the standalone assistant.

Exercise: add a skill without breaking the others

You will add a seventh skill, extend the evaluation set, and prove the router still picks every skill correctly. The report you save is used again when the same order-exception agent is built three ways later in Unit 9.

  1. Open unit09/joule_lab.py.

  2. Above the # ---- the skill catalog line, add an action that lists a customer's orders:

    def act_customer_orders(p):
        found = [o["SalesOrder"] for o in ORDERS.values() if o["SoldToParty"] == p["customer"]]
        return {"customer": p["customer"], "orders": found}
  3. Inside SKILLS, after show_credit_exposure, add this entry:

        "show_customer_orders": {
            "kind": "informational",
            "description": "List which orders one customer placed.",
            "parameters": ["customer"],
            "action": act_customer_orders,
            "reply": lambda r: f"Customer {r['customer']} orders: " + ", ".join(r["orders"]),
            "confirm": False,
        },
  4. In EVAL, add two lines before the meeting-room line:

        ("which orders did customer 10023 place", "show_customer_orders"),
        ("show orders placed by customer 10051", "show_customer_orders"),
  5. Run python unit09/joule_lab.py ask "which orders did customer 10023 place" and check the reply lists 4711 and 4730.

  6. Run python unit09/joule_lab.py eval --out unit09/joule_eval.json. If an older line now misses, the new description is stealing requests: reword it and run again.

Done when skills lists seven skills, the ask in step 5 names orders 4711 and 4730, and eval scores at least 14 of 15 with every line except the finance paraphrase marked OK.

Check yourself

Pick one answer for each question. The explanation appears after you choose.
  1. 1In the mental model, what does Joule Studio add when you deploy a skill or agent?

    Answer: B. SAP's golden path says deploying creates Joule artifacts such as scenarios and dialog functions and registers the agent in Joule's catalog. Joule then routes requests to catalog entries; it doesn't get a new instance or a new model.
  2. 2In the lab, why does route return no skill when two descriptions score the same?

    Answer: C. A tie means the request fits two skills equally well, so any choice is a guess. Declining and asking is safer, which is also why the meeting-room request correctly returns no skill.
  3. 3In the lab, a new skill described as "Manage orders" starts taking requests meant for show_sales_order. Why?

    Answer: D. The lab's router scores descriptions, and a broad one competes with every narrower skill. The fix is a specific description and a re-run of the evaluation set to prove nothing else moved.
  4. 4How does SAP's golden path say a Joule skill reaches an SAP S/4HANA OData API?

    Answer: A. The golden path's build steps configure destinations through action projects that wrap OData APIs, with data mapped in the skill editor and Formula Editor. That keeps skills on APIs, which also fits clean core.
  5. 5A skill creates purchase requisitions. A tester says "it asks for confirmation, so it's secure". What is missing?

    Answer: B. Confirmation, like the lab's --confirm yes, prevents accidents but doesn't check who confirmed. The action must run as the signed-in user so SAP authorizations decide what they may create.
  6. 6Which statement about the new Joule Studio is accurate as of 6 October 2026?

    Answer: D. The Sapphire 2026 guide said early adoption with GA expected in Q3 2026, and the August 2026 reference architecture says some capabilities are not yet GA. The new Joule Studio adds pro-code development with the CLI and frameworks rather than removing anything.
  7. 7Your team has a tested Python agent for order exceptions and needs it inside Joule soon. What would you do?

    Answer: C. SAP's golden path describes pro-code agents exposing an A2A server endpoint so Joule can call them. That keeps your tested code and its evaluation, while users reach it through Joule.
  8. 8Why does the lab's agent stop after one step for order 4723?

    Answer: D. The agent reads the order first and decides the next step from what it finds. An incomplete-data block needs master data fixed, not a credit review, which is the kind of judgement that makes this an agent rather than a skill.

Sources

Sign in to track your progress

We'll email you a one-time sign-in link. No password needed.

or

Tell us a little about you

Optional, every field. It helps us pitch answers to your questions at the right level and decide which topics to write next. It is never shown publicly, and you can change or clear it anytime from the account menu.

SAP areas you work in