When your Groovy script executes inside SAP Advanced Workflow, the workflow engine automatically injects six objects into your script's scope. These are not objects you create — they are provided by the runtime, pre-populated with data from the ICM system. Understanding each object, its properties, its APIs, and the boundaries of what it provides is the foundation of writing correct SAP Advanced Workflow Groovy.
This lesson explores each context object in depth. You'll learn what each one does, which properties are available, how to access them safely, and what happens when you try to access something that doesn't exist. This knowledge is your safety net.
The PARTICIPANT Object: Your Primary Data Source
The participant object represents the person (ICM participant) that the workflow is executing for. This is typically the employee whose compensation plan is being routed through the approval workflow. The participant object is your primary source of data about the individual.
Key Properties of the Participant Object
| Property | Type | Nullable? | Example Value |
|---|---|---|---|
| participant.id | String | No | "EMP001234" |
| participant.name | String | No | "John Smith" |
| participant.employeeId | String | No | "HR-5678" |
| participant.managerId | String | No | "MGR-9999" (the ID only) |
| participant.manager | Participant object | Yes | Participant object or null |
| participant.position | String | No | "Sales_Manager" |
| participant.status | String | No | "ACTIVE" or "INACTIVE" |
| participant.planAssignments | List | No (but may be empty) | [ ... ] |
Using Participant Properties in Code
The most straightforward use of the participant object is accessing its properties to make routing or validation decisions. Here are concrete examples:
// Get the participant's full name and ID def participantName = participant.name def participantId = participant.id log.info("Processing participant: " + participantName) // Use the ID for routing if (participantId == "EMP-CEO") { // Special handling for the CEO route.setApprover("BOARD_SECRETARY") }
Accessing participant.id and participant.name is safe — these properties always exist and are never null. The data type is always a String.
// Route based on participant's position type def position = participant.position log.info("Participant position: " + position) if (position == "Executive") { route.setApproverByRole("CFO") } else if (position == "Manager") { route.setApproverByRole("DIRECTOR") } else { route.setApproverByRole("MANAGER") }
The participant.position property tells you the participant's position type (Executive, Manager, Individual Contributor, etc.). Use this to implement position-based routing logic.
The Manager Object: The Nullable Field
This is the most important concept about the participant object. The participant.manager field is a Participant object that represents the participant's direct manager. Unlike participant.id or participant.name, the manager field can be null.
The manager is null in these scenarios:
- The participant is at the top of the organizational hierarchy (e.g., the CEO, head of department).
- The participant is a new hire whose manager hasn't been assigned yet.
- The participant's manager record was deleted or deactivated in the system.
- The manager field is simply not populated for this participant.
This is a critical source of bugs. If you write code that assumes the manager always exists, your code will crash with a NullPointerException when it encounters a participant with no manager.
// This code is UNSAFE — it will crash if manager is null def managerName = participant.manager.name // If participant.manager is null, this throws NullPointerException // The workflow step fails silently route.setApprover(participant.manager.id)
This code looks innocent, but it's deadly. If participant.manager is null, trying to access .name or .id on it immediately throws a NullPointerException. In the SAP Advanced Workflow sandbox, this exception doesn't cause a visible error — the step just fails silently, and the workflow hangs or routes incorrectly.
// Safe navigation operator (?.) returns null instead of throwing NPE def managerId = participant?.manager?.id // If manager is null, managerId is null. No exception. if (managerId != null) { route.setApprover(managerId) } else { // No manager — route to a default approver route.setApproverByRole("COMPENSATION_ADMIN") }
The safe navigation operator (?.) is Groovy's built-in defense against null. When you write participant?.manager?.id, you're saying "access manager only if participant is not null, and then access id only if manager is not null. If either is null, return null instead of throwing an exception." This pattern is essential in SAP Advanced Workflow.
// Get manager ID safely, with a fallback def managerId = participant?.manager?.id ?: "DEFAULT_APPROVER" log.info("Routing to manager: " + managerId) // Route to the manager, or to default if no manager exists route.setApprover(managerId)
This is the pattern you'll use constantly: get the manager ID with safe navigation, provide a fallback using the Elvis operator (?:), and then route. If the manager exists, route to them. If not, route to a default approver.
Plan Assignments: The List Field
The participant.planAssignments property is a List of plan assignment objects. This represents all the compensation plans assigned to the participant in the current period. The list itself is never null, but it can be empty — a crucial distinction.
// planAssignments is always a List, but may be empty def assignments = participant.planAssignments log.info("Number of plans: " + assignments.size()) if (assignments.isEmpty()) { // No plans assigned to this participant log.warn("Participant has no plan assignments") } else { // Participant has at least one plan log.info("Processing " + assignments.size() + " plans") }
The PLAN Object: The Compensation Plan
The plan object represents the compensation plan currently in context — the plan that the workflow is routing or validating. This is typically a single-year or single-period incentive plan for which the participant's compensation is being calculated.
Key Properties of the Plan Object
| Property | Type | Nullable? | Example Value |
|---|---|---|---|
| plan.id | String | No | "PLAN-2026-SALES" |
| plan.name | String | No | "2026 Sales Incentive" |
| plan.periodId | String | No | "2026-Q2" |
| plan.status | String | No | "DRAFT", "ACTIVE", or "CLOSED" |
| plan.startDate | Date | No | 2026-01-01 |
| plan.endDate | Date | No | 2026-12-31 |
| plan.components | List | No (but may be empty) | [ ... ] |
| plan.totalIncentiveAmount | BigDecimal | No | 250000.00 |
Using Plan Properties for Routing Decisions
The plan object is typically used to make routing decisions based on plan values or status. Here are practical examples:
// Read plan metadata def planName = plan.name def planStatus = plan.status def planId = plan.id log.info("Processing plan: " + planName + " (Status: " + planStatus + ")") // Only approve ACTIVE plans if (planStatus == "ACTIVE") { log.info("Plan is active, proceeding with approval") } else { log.warn("Plan is not active, rejecting") }
// Get the total incentive amount and route based on threshold def totalAmount = plan.totalIncentiveAmount ?: 0 log.info("Total incentive: " + totalAmount) if (totalAmount > 500000) { // Large plan — VP approval route.setApproverByRole("VP_COMPENSATION") } else if (totalAmount > 100000) { // Medium plan — Director approval route.setApproverByRole("DIRECTOR_COMPENSATION") } else { // Small plan — Manager approval route.setApprover(participant?.manager?.id ?: "MANAGER_DEFAULT") }
This routing pattern combines plan data (total incentive amount) with participant data (manager) to implement a tiered approval structure. Plans under 100k go to the manager, plans 100k–500k go to a director role, and plans over 500k go to a VP role.
The WORKFLOW Object: The Instance Context
The workflow object represents the current workflow instance — the execution of this workflow for this participant and plan. It holds metadata about the workflow run, including who triggered it, when, and any variables or comments passed through the workflow.
Key Properties of the Workflow Object
| Property | Type | Nullable? | Example Value |
|---|---|---|---|
| workflow.id | String | No | "WF-001-2026" |
| workflow.name | String | No | "Compensation Plan Approval 2026" |
| workflow.stepName | String | No | "Manager Approval" |
| workflow.submittedBy | String | No | "USER-5555" (user ID of initiator) |
| workflow.submittedDate | Date | No | 2026-04-12 |
| workflow.comments | String | Yes | "Approved with minor adjustments" or null |
Using Workflow Properties for Notifications and Logging
The workflow object is useful for tracking who initiated the workflow and for sending notifications back to them. Here are examples:
// Log workflow execution context log.info("Workflow ID: " + workflow.id) log.info("Current step: " + workflow.stepName) log.info("Submitted by: " + workflow.submittedBy) log.info("Submitted on: " + workflow.submittedDate)
// Build a notification to send back to whoever initiated this workflow def initiatorId = workflow.submittedBy notification.setSubject("Your compensation plan has been approved") notification.setBody( "Your plan " + plan.name + " was approved on " + workflow.submittedDate + ". Total incentive: " + plan.totalIncentiveAmount ) notification.addRecipient(initiatorId) notification.send()
The Comments Field: Nullable
The workflow.comments field holds any comments entered by an approver during the workflow. This field can be null — the approver may have completed their action without entering any comments.
// Comments may be null — handle safely def approverComments = workflow.comments ?: "No comments" log.info("Approver comments: " + approverComments)
The ROUTE Object: The Routing Controller
The route object is your interface to the workflow engine's routing logic. It's not a data object — it's a controller that lets you programmatically direct the workflow to specific approvers or roles.
Key Methods of the Route Object
| Method | Parameter | Effect |
|---|---|---|
| route.setApprover(String userId) | User ID (e.g., "USER-5555") | Route to a specific user |
| route.setApproverByRole(String roleName) | Role name (e.g., "DIRECTOR") | Route to a user with the specified role |
| route.approve() | None | Programmatically approve the step |
| route.reject(String reason) | Rejection reason string | Programmatically reject the step |
Using the Route Object for Routing Decisions
// Get manager ID and route to them def managerId = participant?.manager?.id if (managerId) { log.info("Routing to manager: " + managerId) route.setApprover(managerId) } else { log.warn("No manager found, routing to admin") route.setApprover("ADMIN-DEFAULT") }
// Route based on participant position to appropriate role def position = participant.position if (position == "Executive") { route.setApproverByRole("CFO") } else if (position == "Manager") { route.setApproverByRole("DIRECTOR") } else { // Use manager for individual contributors route.setApprover(participant?.manager?.id ?: "MANAGER_DEFAULT") }
The LOG Object: Your Diagnostic Tool
The log object is your window into what your script is doing. It provides three logging methods that write to the workflow execution log — a persistent record that you can review to debug failed workflows.
Log Methods
| Method | Level | Use Case |
|---|---|---|
| log.info(String message) | INFO | Normal flow — routing decisions, successful checks |
| log.warn(String message) | WARN | Unexpected but non-fatal conditions — no manager, empty list |
| log.error(String message) | ERROR | Conditions that may cause failure — validation failed, missing data |
Logging Best Practices in SAP Advanced Workflow
// Log every step of your routing logic log.info("START: Processing routing for participant " + participant.id) log.info("Participant manager ID: " + (participant?.manager?.id ?: "NONE")) log.info("Plan total: " + plan.totalIncentiveAmount) def managerId = participant?.manager?.id if (managerId) { log.info("Routing to manager: " + managerId) route.setApprover(managerId) } else { log.warn("No manager assigned, using default approver") route.setApprover("DEFAULT-APPROVER") } log.info("END: Routing complete")
The key principle: log before every decision point, and log the values you're about to use. This creates a paper trail that makes debugging failed workflows much easier.
The NOTIFICATION Object: Building Dynamic Messages
The notification object is your interface to the workflow notification system. It allows you to programmatically build and send dynamic email or in-app notifications based on workflow context data.
Key Methods of the Notification Object
| Method | Parameter | Effect |
|---|---|---|
| notification.setSubject(String subject) | Email subject line | Sets the notification subject |
| notification.setBody(String body) | Email/message body | Sets the notification content |
| notification.addRecipient(String userId) | User ID | Adds a recipient (call multiple times for multiple recipients) |
| notification.send() | None | Sends the notification |
Building Personalized Notifications
// Build a notification with participant and plan details def subject = "Plan Approval: " + participant.name + " - " + plan.name def body = "The compensation plan for " + participant.name + " is ready for approval.\n\n" + "Plan: " + plan.name + "\n" + "Total Incentive: " + plan.totalIncentiveAmount + "\n" + "Period: " + plan.periodId notification.setSubject(subject) notification.setBody(body) notification.addRecipient(participant?.manager?.id ?: "HR_ADMIN") notification.send()
// Send a notification to multiple recipients def subject = "Plan Approved: " + participant.name def body = "The compensation plan for " + participant.name + " has been approved." // Send to the participant notification.addRecipient(participant.id) // Send to the manager if (participant?.manager?.id) { notification.addRecipient(participant.manager.id) } // Send to HR notification.addRecipient("HR_TEAM") notification.setSubject(subject) notification.setBody(body) notification.send()
Context Objects Reference Summary
This table summarizes all six injected context objects, what type of data they hold, and their primary use in workflows:
| Object | Purpose | Primary Fields | Typical Use |
|---|---|---|---|
| participant | The person the workflow is for | id, name, manager, position, status, planAssignments | Routing decisions, identifying the person, accessing manager |
| plan | The compensation plan | id, name, status, totalIncentiveAmount, components, startDate, endDate | Threshold-based routing, plan validation, notification content |
| workflow | The workflow execution instance | id, name, stepName, submittedBy, submittedDate, comments | Tracking who initiated the workflow, sending notifications to initiator |
| route | Routing controller | setApprover(), setApproverByRole(), approve(), reject() | Directing the workflow to the next approver or role |
| log | Diagnostic logging | info(), warn(), error() | Debugging failed workflows, tracking execution flow |
| notification | Notification builder | setSubject(), setBody(), addRecipient(), send() | Building and sending dynamic notifications |
What's Not Available
Understanding what you don't have is just as important as understanding what you do have. Here's what is NOT available in the SAP Advanced Workflow sandbox:
The Null Safety Principle
The most critical principle when working with context objects in SAP Advanced Workflow is always assume that any field except the primary identifier can be null. The manager field is a perfect example — it's an object reference that can legitimately be null. Always use safe navigation (?.) when accessing nested properties, and always provide fallbacks using the Elvis operator (?:).
This principle will save your workflow from silent failures and keep your routing logic robust across all edge cases.