TL;DR
The workflow engine injects six objects into your Groovy script scope: participant, plan, workflow, route, log, and notification — these are your only tools.
Each object has specific properties and methods; accessing a property that doesn't exist or on a null object causes a NullPointerException that breaks your routing silently.
The participant object is your primary data source — it holds the person the workflow is running for, their manager, position, and plan assignments. The manager field can be null (no manager, top of org tree), and you must defend against this.

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:

Accessing Participant ID and Name
// 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.

Using Employee ID and Position for Routing
// 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.

WRONG: Unsafe Access to Manager (Will Break)
// 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.

RIGHT: Safe Access to Manager Using Safe Navigation (?.)
// 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.

Using Manager ID for Routing (With Null Handling)
// 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.

Checking Plan Assignments
// 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:

Reading Plan Name and Status
// 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")
}
Routing by Plan Value (Threshold-Based)
// 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:

Logging Workflow Metadata
// 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)
Sending a Notification Back to the Workflow Initiator
// 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.

Safely Accessing Workflow 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

Route to a Specific User (Manager)
// 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 by Role
// 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

Comprehensive Logging for Routing
// 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

Simple Personalized Notification
// 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()
Multi-Recipient Notification
// 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:

⚠️No database access: You cannot create SQL connections, execute queries, or access SAP HANA directly. The data you need must come from the six injected context objects. If you need data beyond these objects, your workflow architecture must be designed to pass that data as input before the Groovy step runs.
⚠️No HTTP or external APIs: You cannot make HTTP requests, REST calls, or access external services. The workflow engine provides all the APIs you need through the context objects.
⚠️No file I/O: You cannot read or write files, access the file system, or manage directories. Workflows execute in a stateless, sandboxed environment.
⚠️No arbitrary Java imports: You cannot import java.sql.*, java.net.*, or any other Java libraries not explicitly provided by the workflow engine. Attempting to do so fails silently or with cryptic errors.

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.