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 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:
// 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:
// 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():
// 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
// 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()
// 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 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:
// 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:
// 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:
// 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:
Step: Manager Approval Status: Failed [No helpful error message, script just failed silently]
Now add comprehensive 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:
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:
// 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")