Mustafa.
Back to all projects

HanseWatt — Agentforce & Einstein AI service for DACH energy

An AI customer-service platform for a DACH energy retailer: a live, German-speaking Agentforce agent that runs the full process end to end — identifies the customer, explains their bill grounded in Data 360, and logs a support Case — under the Einstein Trust Layer. Further phases in development.

Year
2026
Role
Salesforce Developer
Tech stack
Service CloudAgentforce (ReAct planner)Data 360 (Data Cloud)Data 360 Query API (SQL Calculated Insight)Einstein Trust Layer@InvocableMethod (grounded action)ApexKnowledgeOmni-ChannelSLA EntitlementsSFDX

Highlights

  • The claim the project actually makes: a prompt is not a security mechanism — only code is. Three guarantees I first only hoped for, then rebuilt as code paths that cannot be skipped
  • Consent is a state machine, not a Boolean: the FIRST confirmation of a tariff change is structurally incapable of applying it (applied: false) — it returns the binding terms, so the customer is guaranteed to see them. Only a second call, against a server-issued expiring request number, switches the contract
  • Identity is a token, not a claim: an email alone releases nothing — the customer must also present the Kundennummer from their bill. To see another customer's data you'd need THEIR number; the model cannot invent it. A failed attempt doesn't even reveal whether the email is registered
  • The agent knows when it may not be sure: German tariffs are per-kWh + monthly base fee, so the 'cheaper' tariff can cost more. isConditional flips true/false across two real customers — Lena gets a conditional recommendation ('is the higher usage permanent?'), Studio Alpina gets an unconditional one. Same Apex, opposite answers
  • An action input is a string an LLM chose: when the planner put an EMAIL in the Account Id slot, Apex threw before our code ran and the customer saw a stack trace where a privacy refusal belonged. HWIds now parses every id at the boundary — a bad id is a refusal, never an exception
  • Live Agentforce agent (ReAct planner), deployed + source-controlled: 4 grounded Apex @InvocableMethod actions (of 8 total) — verify (2FA) · latest bill · explain the +64.6 % anomaly (Data 360) · cited Knowledge answer · propose tariff · confirm tariff · create case · register a move — every one bulk-safe, with sharing, WITH USER_MODE
  • 115 Apex tests / 97 % coverage, and the ones that matter assert the REFUSALS: the first confirmation changes nothing, an invented / foreign / expired request number runs zero DML, a move is never filed on an invented date. Every rule proven before the agent runs it — at zero Flex Credits
  • Adversarially tested — and hardened. I pointed my own red-team bench (Prüfstand) at this agent. It found three weaknesses; I closed two with structural guardrails and published the third as still open rather than bury it. The agent that ships is the one that survived being attacked by its author
  • Honest framing: external systems (smart-meter MDM, SAP IS-U) are simulated — the Platform Event seam to them is real and source-controlled. Phase-1 core: 7 custom objects (37 fields), SLA entitlements, Omni-Channel, 10-article Knowledge base, multi-currency EUR/CHF; designed across 19 ADRs

Feature walkthrough

One agent, the whole service conversation

The HW Energy Agent in Agentforce Builder: two subagents (Account Information Assistance, High Energy Consumption Insights), the live ReAct reasoning trace in the middle, and the multi-turn conversation on the right. In a single session it identifies the customer, retrieves the latest bill, explains the consumption anomaly, and opens a case — a ReAct planner chaining custom Apex actions, all under the Einstein Trust Layer.

HW Energy Agent in Agentforce Builder — two subagents, the ReAct reasoning trace, and the live multi-turn conversation

Grounded, not hallucinated

The agent never invents a number. The reasoning trace shows it calling the HWExplainConsumptionAction Apex action, which returns the real figures — latest 520 kWh vs a 316 kWh trailing average, +64.6%, cited to “Data 360 — Meter Reading” — and Salesforce’s own Output Evaluation marks the reply GROUNDED. Every figure the customer sees is traceable to a record, not the model’s imagination.

Agent reasoning trace — HW Explain Consumption Anomaly action returns 520 vs 316 kWh, +64.6%, cited to Data 360, with a GROUNDED verdict

Answers from the customer’s real bill

Ask “how much is my latest bill?” and the agent first identifies the customer from the email they give, then reads their actual Energy_Bill__c — BILL-000004, €176.30 for 520 kWh, 1–31 May 2026, status Open. The lookup runs in a bulk-safe Apex service with WITH USER_MODE, so the agent only ever sees what the running user is allowed to. In this demo the customer types their email; in production the agent recognises them from the channel itself — the incoming WhatsApp number or email sender, resolved to a Data 360 unified profile — so the very same multi-turn flow runs over WhatsApp or email with no login (designed, next).

Agent answering the customer's latest bill — BILL-000004, €176.30 for 520 kWh, status Open

Explains the +64.6 % spike from Data 360

“Why is it so high?” triggers the consumption-anomaly action: the latest 520 kWh is 64.6 % above the customer’s 316 kWh trailing average — a recent, pronounced jump consistent with a new high-draw appliance such as an EV charger or heat pump. The same +64.6 % is independently proven in Data 360 via a SQL Calculated Insight over the Meter Reading DLO.

Agent explaining the +64.6% consumption anomaly — 520 kWh vs a 316 kWh trailing average

Takes real action — opens a support case

The agent doesn’t just explain; it acts. “This is wrong — open a complaint” makes it call HWCreateCaseAction, which inserts a real Case (as the running user, WITH USER_MODE), categorised by topic, and replies with the case number. The case flows into the Phase-1 SLA entitlement + Omni-Channel routing for a human specialist.

Agent opening a complaint case and replying with the case number

A real Case, created by the agent

Proof the action landed: a Case in Salesforce — “Complaint: High Electricity Bill for May 2026”, Created By EinsteinServiceAgent User, linked to Lena Bergmann’s account, EUR currency, with a description grounded in her actual bill. Not a mock — a governed record a service rep picks up next.

The Case the agent created in Salesforce — Created By EinsteinServiceAgent User, linked to Lena Bergmann

The figures are real records

The €176.30 / 520 kWh the agent quoted is exactly what’s stored: the Energy_Bill__c table shows BILL-000004 for Lena Bergmann next to the other DACH customers (Huber, Müller, Studio Alpina), each in EUR. The agent reads the system of record — it doesn’t paraphrase a guess.

The Energy Bill records in Salesforce — BILL-000004 for Lena Bergmann, €176.30, 520 kWh, alongside the other DACH customers

The anomaly is real meter data

And the +64.6 % is grounded too: Lena’s Meter_Reading__c history shows five months around 316 kWh (310 / 325 / 300 / 330 / 315) and then a 520 kWh spike — the exact series the anomaly action averages and compares. The same data lives in the Salesforce core and the Data 360 Meter Reading DLO.

The Meter Reading records — Lena's five months around 316 kWh and then the 520 kWh spike

Identity is a token, not a claim

An email alone releases nothing. HW Verify Customer requires the email AND the Kundennummer printed on the customer's bill — the way a German utility hotline actually authenticates. Both go in; only then does an Account Id come back, and every other action hangs off that Id. So to see another customer's data you need their customer number: the person asking doesn't have it, and the model can't invent it — it is compared server-side against a stored value that never appears in any output. A failed attempt reveals nothing, not even whether the email is registered, so the agent can't be used to enumerate customers.

HW Verify Customer trace — both the email AND the customer number go in, verified: true comes back

The Grundgebühr trap — and an agent that knows it may not be sure

German tariffs are Arbeitspreis (per kWh) PLUS Grundpreis (a fixed monthly fee), so the cheaper headline price can be the more expensive tariff. The action costs every comparable tariff at the customer's own consumption, in both scenarios: at Lena's 316 kWh trailing average, Strom Basis wins (1332.24 €); at her recent 520 kWh, the EV tariff wins (2046.00 € vs 2115.60 €) — because its lower 0.28 €/kWh finally beats its higher 24.90 € base fee. Her usage straddles the 375 kWh/month break-even, so the honest answer depends on a fact the agent does not know. isConditional: true — and the agent states the assumption and ASKS instead of recommending. A Flow can do the arithmetic; it cannot notice that the arithmetic is undetermined.

HW Propose Tariff Change trace for Lena — isConditional: true, with every tariff costed at both consumption levels

Same question, different customer, opposite answer

Studio Alpina uses 165 kWh/month — below the break-even — so the 'cheaper' EV tariff is genuinely more expensive for them: the base fee eats the saving. The agent recommends Strom Basis, not EV, with a 103.20 € annual saving. And the flag flips: isConditional: false — 'Basis is cheapest in BOTH scenarios, so the recommendation does not depend on whether the higher usage persists.' Opposite tariff, opposite epistemic stance, same Apex. That is not a turn of phrase — it is a field the code computes and the agent reads. No hallucination produces it.

The same action for Studio Alpina — isConditional: false, Strom Basis recommended, 103.20 EUR saving

The second confirmation is a state machine, not an instruction

The topic instruction said 'ask twice'. In the first recording the agent applied the change on the FIRST 'Ja' — because nothing stopped it. An instruction is precisely the trap the design exists to name. So the second confirmation became a state: Proposed → Terms_Presented → Applied. Here is the agent calling the action, and the code refusing: applied: false, awaitingFinalConfirmation: true, 'NOTHING HAS BEEN CHANGED YET.' The model wanted to switch. The code did not let it. Instead it returned the binding terms — the tariff, the effective date, the saving, and the assumption the saving rests on — so the customer is guaranteed to see them. Not because the agent was asked nicely; because there is no code path that skips them.

HW Confirm Tariff Change, first call — applied: false, awaitingFinalConfirmation: true, 'NOTHING HAS BEEN CHANGED YET'

Only the second call applies it

The same action, called a second time with the same server-issued request number, after the customer has explicitly accepted the binding terms: applied: true. It switches the contract, opens a categorised Case, and publishes a Tariff_Change_Requested__e platform event. And the request number is the reason the whole thing is safe: it exists only in the output of the propose step, so a model that skipped the proposal has nothing to present; a model that invents 'TCR-99999' is refused because no such row exists; a model that replays another customer's number is refused because the query is account-scoped. Consent is enforced by the data flow, not by hoping the prompt held.

HW Confirm Tariff Change, second call — applied: true, case 00001046 opened

Consent is an auditable row, not a Boolean

The tempting design is a customerConfirmed Boolean on the write action — and then a README that claims 'human-in-the-loop is enforced in code'. That claim is false: the Boolean is filled in by the model. So consent here is a server-issued, expiring, account-scoped record. TCR-00012: Applied, EUR 69.60, expires at 19:09, applied at 18:40, created by the EinsteinServiceAgent User — and, in its own field, the assumption the quote depends on. Who agreed, when, to which proposal, at which price, on what stated assumption. A Boolean cannot answer any of those questions.

Tariff_Change_Request__c TCR-00012 — Applied, EUR 69.60, with the assumption the quote depends on

And the system really changed

Same record, SC-000004. Before the conversation: HanseWatt Strom Basis. After it: HanseWatt Strom EV-Tarif — 'zuletzt geändert von EinsteinServiceAgent User'. The agent didn't describe a tariff change; it performed one. The far side of the billing boundary is honest about itself: the Salesforce contract is switched here, and a Tariff_Change_Requested__e platform event is published for a MuleSoft subscriber to forward to SAP IS-U. SAP is simulated — the integration seam is real, published, and source-controlled. 'I built the integration point; the far side is simulated' is a materially different claim from 'I simulated it'.

Lena's Service Contract SC-000004 after the change — HanseWatt Strom EV-Tarif, last modified by the EinsteinServiceAgent User

The privacy refusal, and why it isn't politeness

A verified customer asks for a NEIGHBOUR's bill — with the neighbour's real, registered email — and then insists: 'he said it's fine'. Read the trace carefully: the classifier picked the RIGHT topic, the agent had 8 actions available including HW Get Latest Bill — and it called NONE of them. Salesforce's own Output Evaluation marks the refusal GROUNDED: 'account data for other customers cannot be shared due to data protection policies.' And even if the planner had tried, it would have failed: without the neighbour's Kundennummer no Account Id is ever issued, so no downstream action has anything to ground on. DSGVO here is not a sentence in a prompt. It is the absence of a path.

The privacy refusal trace — the correct topic was selected with 7 actions available, and none were called; Salesforce marks the refusal GROUNDED

A move is filed, not narrated

The agent used to explain a move from a cited Knowledge article and then had nowhere to put the customer's answer: no record, no reference, the conversation left hanging. Now it files one — but only once the customer supplies the facts only THEY have. 'Ich ziehe nächsten Monat um' is not a date, and the agent does not turn it into one: a missing move-out date or final meter reading is a refusal with zero DML, because an estimated reading becomes an estimated final bill — the exact thing the customer is trying to avoid. Given the real facts it opens a Move case carrying the date, the customer's own reading (41250 kWh, 'not estimated'), and the new address.

The Move case the agent filed — move-out date, the customer's own final meter reading, and the new address

The problem

DACH energy retailers field a flood of repetitive, German-language service contacts — 'why is my bill so high?' — that need accurate, data-grounded answers and safe, auditable action, not a chatbot that guesses. The hard part is grounding an LLM in unified consumption + billing data, letting it take real action (create cases, initiate tariff changes), and escalating safely to a human — all GDPR/DSGVO-compliant under the Einstein Trust Layer.

Architecture

Customer channels (web chat, WhatsApp, Experience Cloud) reach an Agentforce Service Agent — topics, actions, and instructions running under the Einstein Trust Layer (PII masking, grounding, audit). Grounding (RAG) pulls from German Knowledge articles and Data 360 DMOs; an Apex action layer (HWIdentifyCustomer, HWGetLatestBill, HWExplainConsumption, HWCreateCase) takes real action against the Service Cloud core (Account, Contact, Case with record types + SLA, Knowledge, Omni-Channel) and custom objects (Meter__c, Tariff__c, Service_Contract__c, Energy_Bill__c…). Underneath, Data 360 ingests smart-meter and SAP IS-U billing data (Ingestion API / MuleSoft) → DLO → DMO → identity resolution → unified profile, with calculated insights (avg kWh, anomaly, churn) driving segments and proactive outreach — closing the loop from reactive service to proactive prevention.

The approach

Built in gated phases with honest framing: external systems (smart-meter MDM, SAP IS-U) are simulated, while the Salesforce code paths, the agent, the Data 360 model, and the grounding are real. The SFDX org is split into eight packages (core, services, actions, handlers, agent, datacloud, lwc, tests) so architectural boundaries are visible at the filesystem level, and every trigger extends the Kevin O'Hara TriggerHandler framework. Phase 1 (Service Cloud core) is deployed; beyond it, a live Agentforce agent (ReAct planner) is deployed and source-controlled, Data 360 grounding is proven via the Query API (the anomaly Calculated-Insight written as SQL), and it now answers grounded and takes real action — a full multi-turn flow that identifies the customer (email → Account), retrieves the latest bill, explains a consumption anomaly grounded in Data 360 (+64.6 % vs the trailing average, proven via the Query API), and logs a support Case, backed by four grounded Apex @InvocableMethod actions (HWIdentifyCustomer, HWGetLatestBill, HWExplainConsumption, HWCreateCase) each with a bulk-safe service + test. Identity resolution, LWCs, prompt templates, escalation flows, and agent evaluation + red-teaming are designed (ADRs) and next.

The outcome

Phase 1 is live: a 7-object energy data model (37 fields), Case management with record types and an SLA entitlement (4h first response, 2-business-day resolution), Omni-Channel routing with German + Billing skills, a 6-topic Knowledge base of 10 published articles, three permission sets, and multi-currency (EUR + CHF) with seeded DACH demo data — documented across 19 Architecture Decision Records. Beyond Phase 1, a live Agentforce agent now answers grounded and takes real action — four @InvocableMethod actions running a multi-turn flow that identifies the customer, explains a Data 360-grounded consumption anomaly (+64.6 %) and logs a support Case — with documented Flex-Credit cost governance; identity resolution, LWCs and agent eval/red-team are next. It's the 'serve' half of the enterprise that complements TechnoStore's 'sell' half.

Watch the demoGitHub & Notion — private, walkthrough on request