TL;DR
Dynamic notifications use Groovy string interpolation to build personalized email/message content from context objects. Use GStrings (${variable}) for inline interpolation and always null-safe context values with Elvis operators.
Debugging failed workflows requires systematic methodology: add comprehensive logging, read the workflow log, identify the failure mode (never fires, hangs, fails silently, routes wrong), and trace back through logs to find the issue.
Logging is your only debugging tool in SAP Advanced Workflow — you cannot attach a debugger or inspect runtime state. Log everything: START/END markers, every value used, every decision, every error.

At the end of a workflow, users need to know what happened. A notification — email or in-app message — communicates the result, next steps, and any required actions. This lesson teaches you to build personalized, accurate notifications from workflow context data. It also teaches you the debugging methodology that lets you fix failing workflows even when you can only inspect logs.

Dynamic Notifications: Building Personalized Messages

The notification object allows you to programmatically build and send notifications. The key to effective notifications is personalizing them with context data — participant name, plan name, amount, approver, deadline — making each notification relevant and actionable.

Notification Object Methods

Method Parameter Effect
notification.setSubject(String) Email subject line Sets notification subject
notification.setBody(String) Email/message body Sets notification content
notification.addRecipient(String userId) User ID Adds one recipient (call multiple times for multiple)
notification.send() None Sends the notification

Groovy String Interpolation (GStrings)

Groovy's GString allows you to embed expressions directly in strings using ${expression} syntax. This is perfect for building dynamic notification content:

Basic String Interpolation
// Basic interpolation — ${variable} inside double quotes
def name = "John Smith"
def greeting = "Hello ${name}"
log.info(greeting)  // Outputs: "Hello John Smith"

// Expression interpolation
def amount = 250000
def message = "Total incentive: $${amount}"
log.info(message)  // Outputs: "Total incentive: $250000"

Multi-Line Notifications (Triple-Quoted Strings)

For longer notification bodies, use triple-quoted strings, which preserve newlines and allow embedded expressions:

Multi-Line Notification Body
// Triple-quoted string for multi-line content
def body = """
  Dear ${participant.name},

  Your compensation plan has been submitted for approval.

  Plan: ${plan.name}
  Total Incentive: $${plan.totalIncentiveAmount}
  Period: ${plan.periodId}

  Your manager will review and approve within 5 business days.
  If you have questions, contact HR.
"""

notification.setBody(body)

Null-Safe Notification Content

When building notifications with context data, always assume data can be null. Use safe navigation and Elvis operators:

Null-Safe Notification Content
// Use Elvis operators to provide defaults for potentially null values
def managerName = participant?.manager?.name ?: "Your Manager"
def planName = plan.name ?: "Your Plan"
def amount = plan.totalIncentiveAmount ?: 0

def subject = "Plan approval for ${participant.name}"

def body = """
  Plan: ${planName}
  Manager: ${managerName}
  Amount: $${amount}
"""

notification.setSubject(subject)
notification.setBody(body)

Multi-Recipient Notifications

A single notification can be sent to multiple recipients. Call addRecipient() multiple times before calling send():

Send to Multiple Recipients
// Build the notification content
notification.setSubject("Plan Approved: " + participant.name)
notification.setBody("The plan for " + participant.name +
                    " has been approved.")

// Add multiple recipients
// Send to the participant
notification.addRecipient(participant.id)

// Send to the manager (if exists)
if (participant?.manager?.id) {
  notification.addRecipient(participant.manager.id)
}

// Send to HR
notification.addRecipient("HR_TEAM")

// Send all at once
notification.send()

Notification Types: Common Scenarios

Plan Release Notification to All Participants
// Notify participant that plan is ready
notification.setSubject("Your " + plan.name + " is ready")
notification.setBody("""
  Your compensation plan for ${plan.periodId}
  is now available for review.

  Plan: ${plan.name}
  Status: ${plan.status}

  Please log in to review your details.
""")
notification.addRecipient(participant.id)
notification.send()
Approval Dispute Notification
// Notify participant and manager when quota dispute is resolved
def initiatorComments = workflow.comments ?: "Not provided"

notification.setSubject("Quota dispute resolved for " + participant.name)
notification.setBody("""
  Your quota dispute has been reviewed.

  Participant: ${participant.name}
  Plan: ${plan.name}
  Resolution: ${initiatorComments}

  Please contact your manager if you have questions.
""")

notification.addRecipient(participant.id)
if (participant?.manager?.id) {
  notification.addRecipient(participant.manager.id)
}
notification.send()

HTML Content in Notifications

Some SAP Advanced Workflow configurations support HTML in notification bodies. You can use basic HTML tags like <br>, <b>, <a href>:

HTML-Formatted Notification
// HTML-formatted notification body (if supported)
def htmlBody = """
  <p>Dear ${participant.name},</p>

  <p>Your plan has been <b>approved</b>.</p>

  <table>
    <tr><td>Plan</td><td>${plan.name}</td></tr>
    <tr><td>Amount</td><td>$${plan.totalIncentiveAmount}</td></tr>
  </table>

  <p><a href="http://sap.example.com/icm">View in System</a></p>
"""

notification.setBody(htmlBody)

Debugging Groovy Scripts: The Structured Approach

When your workflow fails or routes incorrectly, debugging in SAP Advanced Workflow is different from debugging in a regular IDE. You cannot attach a debugger or inspect runtime state. Your only tool is the workflow execution log. This requires systematic methodology:

Step 1: Add Comprehensive Logging

Before debugging a failed workflow, make sure your script logs everything. Add log statements at every critical point:

Comprehensive Logging Template
// Log at the very start
log.info("START: Routing workflow")

// Log every value you're using
log.info("Participant ID: " + participant.id)
log.info("Manager ID: " + (participant?.manager?.id ?: "NONE"))
log.info("Plan amount: " + (plan.totalIncentiveAmount ?: 0))

// Log before every decision
def amount = plan.totalIncentiveAmount ?: 0
if (amount > 500000) {
  log.info("Amount exceeds 500k, routing to VP")
  route.setApproverByRole("VP")
} else {
  log.info("Amount under 500k, routing to manager")
  route.setApprover(participant?.manager?.id ?: "DEFAULT")
}

// Log at the very end
log.info("END: Routing complete")

Step 2: Read the Workflow Execution Log

When a workflow fails, you must read the workflow execution log in the SAP Advanced Workflow admin interface. The log shows:

  • Step name and status (completed, pending, failed)
  • Execution timestamp
  • All log statements from your Groovy scripts (info, warn, error)
  • Any exceptions that were thrown

Look for your log statements with START and END markers. If you see START but no END, the script failed somewhere in the middle. If you see neither, the script never executed.

Step 3: Identify the Failure Mode

Failed workflows exhibit four distinct failure modes. Identifying which one helps you narrow down the issue:

Failure Mode Symptom Likely Cause Check
Workflow Never Fires Workflow doesn't start at all, no steps executed Event trigger misconfigured, wrong trigger condition Check workflow trigger configuration, participant data that should trigger the workflow
Step Hangs Pending Workflow reaches a step but never moves past it Null approver (routing returned null), null route result, script exception Log statements in the step, check if approver ID is valid, check for null handling
Step Fails Silently Step starts but immediately fails, workflow halts, no error message Groovy exception (NullPointerException, etc.), wrong validation return type, script error Look for exception stack traces in log, check return type for validation scripts
Wrong Person Gets Task Workflow completes but routes to wrong approver Routing logic error, condition evaluated incorrectly, wrong fallback value Check routing log statements, verify condition logic, trace the routing decision

Step 4: Trace Back Through Logs

Once you identify the failure mode, work backward through the log:

Example: Tracing a Routing Failure
// Your script logs:
// "START: Routing workflow"
// "Participant ID: EMP-5555"
// "Manager ID: NONE"  ← Aha! No manager found
// "Plan amount: 250000"
// "Amount under 500k, routing to manager"  ← Should have gone to default
// "END: Routing complete"  ← No error, but task never appeared

// Problem: You routed to manager ID "NONE" literally, instead of the default
// Fix: Use Elvis operator: participant?.manager?.id ?: "DEFAULT"

Testing Outside Workflow: Limited Options

You cannot fully test Groovy outside the SAP Advanced Workflow sandbox — the context objects are not available. But you can do limited testing:

  • Syntax checking: Use a Groovy IDE or console to verify syntax before embedding in the workflow.
  • Unit testing with mocks: Write unit tests that mock the context objects (participant, plan, route) and verify your logic.
  • Sandbox testing: The only real test is in the SAP Advanced Workflow sandbox with actual participant and plan data.

Complete Example: End-to-End Debugging

Here's a real-world debugging scenario:

Buggy Script
// Original script (has a bug)
def managerId = participant.manager.id  // No safe navigation!
route.setApprover(managerId)

This script crashes with NullPointerException when manager is null. The workflow log shows:

Workflow Log Output (Shows Nothing Useful)
Step: Manager Approval
Status: Failed
[No helpful error message, script just failed silently]

Now add comprehensive logging:

Fixed Script with Logging
// Fixed script with logging
log.info("START: Manager routing")

log.info("Participant: " + participant.id)
log.info("Manager: " + participant?.manager?.id)

def managerId = participant?.manager?.id ?: "DEFAULT_APPROVER"  // Safe navigation + Elvis
log.info("Routing to: " + managerId)

route.setApprover(managerId)

log.info("END: Manager routing complete")

Now the log shows:

Workflow Log Output (Clear and Actionable)
Step: Manager Approval
Status: Completed
Logs:
  "START: Manager routing"
  "Participant: EMP-5555"
  "Manager: null"  ← Aha! No manager
  "Routing to: DEFAULT_APPROVER"  ← Using fallback correctly
  "END: Manager routing complete"

The workflow completes successfully, and you know exactly what happened. The logs are your debugging interface.

Debugging Flow: Decision Tree

Observed Behavior First Check If Found If Not Found
Workflow never starts Trigger configuration in workflow definition Fix trigger, redeploy workflow Check participant data matches trigger condition
Step hangs pending Log for "END" marker Script failed partway, find where logs stop Script completed but approver ID is invalid or null
Task appears in wrong person's queue Check routing logic in logs Logic executed but produced wrong decision, fix condition Check if approver ID in log is correct (may be system issue)
Validation blocks valid submission Check validation error message shown to user Message shows which field failed, fix script or data Script may have returned wrong data type, check return statement

Go-Live Groovy Checklist

Before deploying any Groovy script to production, verify:

  • Null Safety: Every manager access uses safe navigation (?.), every list access checks isEmpty(), every optional field has a fallback with Elvis (?: )
  • Return Types: Validation scripts return a Map with valid and message keys, not String or Boolean
  • Logging: Script logs START and END, logs every value used, logs before every decision point
  • Routing Logic: Every condition is tested, fallback approver exists for edge cases (no manager, empty list, null field)
  • Notification Content: Notifications use safe navigation for all context data, recipients are valid user IDs or roles
  • Sandbox Testing: Script has been tested with real participant data, edge case data (new hire, CEO, no manager), and different plan amounts
  • Error Messages: Validation error messages are clear and actionable ("Quota must be > 0" not "Invalid")
  • No Hard-Coded IDs: Approver IDs are derived from context (manager, roles) not hard-coded; hard-coded IDs break when people change roles

Logging Pattern Reference

Use this pattern in every production Groovy script:

Standard Production Logging Pattern
// START marker
log.info("START: [Describe what this script does]")

// Log input values
log.info("Input - Participant: " + participant.id)
log.info("Input - Plan: " + plan.id)

// Log derived values with safe navigation
log.info("Derived - Manager ID: " + (participant?.manager?.id ?: "NONE"))

// Log before decisions
if (condition) {
  log.info("Decision: Taking path A")
  // ... action A
} else {
  log.info("Decision: Taking path B")
  // ... action B
}

// Log output
log.info("Output - Routed to: " + approverId)

// END marker
log.info("END: [Script name] complete")