1. IGA and Workflows — Overview
Identity Governance and Administration (IGA) is the field of identity lifecycle management — from when a user joins the organization (joiner), changes roles (mover), to when he leaves (leaver). Keycloak Workflows is a preview feature that enables the automation of these IGA processes.
1.1 Why do we need Workflows?
| Problem | No Workflows | Yes Workflows |
|---|---|---|
| Onboarding new employees | Admin must manually create account, assign roles, add to groups | Automatically: create account → assign roles according to department → add groups → send welcome email |
| Changing departments | Admin must manually delete old roles and assign new roles | Automatically detect department changes → update corresponding roles/groups |
| Offboarding | Forgot revoke access → security risk | Automatically disable account → revoke sessions → remove from groups |
| Access review | Unable to periodically review | Automatically check and report unused permissions |
1.2 What is Keycloak Workflows?
Keycloak Workflows is a event-driven, condition-based system that allows the definition of automated processes including:
- Trigger: Workflow trigger event (user created, attribute changed, login event...)
- Conditions: Conditions that must be met for the workflow to execute
- Steps: Actions to take when conditions are matched (assign role, add to group, send notification...)
2. Enable Workflows (Preview Feature)
Workflows is feature preview, needs to be enabled explicitly:
# Bật qua command line
bin/kc.sh start --features=workflows
# Hoặc trong keycloak.conf
features=workflows
# Bật cùng các features khác
bin/kc.sh start --features=workflows,organizations,scripts
# Docker
docker run -e KC_FEATURES=workflows \
quay.io/keycloak/keycloak:latest start-dev
Note: Preview features may change API between versions. Should not be used in production without thorough testing.
3. Workflow Concepts
3.1 Workflow Components
Workflow Definition
├── Metadata (name, description, enabled)
├── Trigger (event type)
├── Conditions (when to execute)
│ ├── Condition 1 (attribute check)
│ ├── Condition 2 (group membership)
│ └── ... (AND/OR logic)
└── Steps (what to do)
├── Step 1 (assign role)
├── Step 2 (add to group)
├── Step 3 (send notification)
└── ... (sequential execution)
3.2 Trigger Types
| Trigger | Description | Use case |
|---|---|---|
| USER_CREATED | Newly created user | Onboarding: auto-assign roles/groups |
| USER_UPDATED | User attributes change | Mover: detect department change |
| USER_DELETED | User deleted | Cleanup: revoke external access |
| USER_DISABLED | User disabled | Offboarding: revoke sessions |
| GROUP_MEMBERSHIP_CHANGED | User added/removed from group | Auto-assign related roles |
| ROLE_ASSIGNED | User assigned role | Cascading permissions |
| ROLE_REMOVED | User deleted role | Revoke dependent access |
4. Workflow Definitions
4.1 Workflow Structure Definition
{
"name": "Employee Onboarding",
"description": "Automatically provision new employees based on department",
"enabled": true,
"trigger": {
"type": "USER_CREATED"
},
"conditions": [
{
"type": "user_attribute",
"attribute": "employeeType",
"operator": "equals",
"value": "full-time"
}
],
"steps": [
{
"type": "assign_role",
"role": "employee"
},
{
"type": "add_to_group",
"group": "/Company/All-Employees"
},
{
"type": "conditional",
"condition": {
"type": "user_attribute",
"attribute": "department",
"operator": "equals",
"value": "engineering"
},
"thenSteps": [
{
"type": "assign_role",
"role": "developer"
},
{
"type": "add_to_group",
"group": "/Company/Engineering"
}
]
}
]
}
4.2 YAML Format
# workflow-onboarding.yaml
name: Employee Onboarding
description: Auto-provision new employees
enabled: true
trigger:
type: USER_CREATED
conditions:
- type: user_attribute
attribute: employeeType
operator: equals
value: full-time
steps:
- type: assign_role
role: employee
- type: add_to_group
group: /Company/All-Employees
- type: conditional
condition:
type: user_attribute
attribute: department
operator: equals
value: engineering
thenSteps:
- type: assign_role
role: developer
- type: add_to_group
group: /Company/Engineering
- type: conditional
condition:
type: user_attribute
attribute: department
operator: equals
value: marketing
thenSteps:
- type: assign_role
role: marketer
- type: add_to_group
group: /Company/Marketing
5. Workflow Expression Language
Workflows uses expression language to define dynamic conditions and reference user attributes.
5.1 Variables
| Variable | Description | Example |
|---|---|---|
user.username | Username of user | john.doe |
user.email | Email address | [email protected] |
user.firstName | First name | John |
user.lastName | Last name | Doe |
user.attributes.{name} | Custom attribute | user.attributes.department |
user.groups | List group paths | ["/Engineering", "/VPN-Users"] |
user.roles | List assigned roles | ["employee", "developer"] |
event.type | Event type | USER_CREATED |
event.time | Event timestamp | 2026-03-30T10:00:00Z |
5.2 Operators
| Operator | Description | Example |
|---|---|---|
equals | Compare equals | user.attributes.department equals "engineering" |
not_equals | Not equal | user.attributes.status not_equals "inactive" |
contains | Contains values | user.email contains "@acme.com" |
starts_with | Starts with | user.username starts_with "svc-" |
ends_with | Ends with | user.email ends_with "@acme.com" |
in | Belongs to list | user.attributes.location in ["HCM", "HN", "DN"] |
exists | Attribute exists | user.attributes.employeeId exists |
not_exists | Attribute does not exist | user.attributes.termination_date not_exists |
5.3 Functions
# String functions
upper(user.attributes.department) → "ENGINEERING"
lower(user.email) → "[email protected]"
trim(user.attributes.title) → "Senior Developer"
# Date functions
now() → current timestamp
daysAgo(30) → timestamp 30 ngày trước
daysBetween(user.createdTimestamp, now()) → số ngày từ lúc tạo
# Collection functions
size(user.groups) → số groups
contains(user.roles, "admin") → true/false
6. Managing Workflows
6.1 CRUD Operations
# Tạo workflow
curl -X POST "http://localhost:8080/admin/realms/my-realm/workflows" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "Employee Onboarding",
"description": "Auto-provision new employees",
"enabled": true,
"trigger": { "type": "USER_CREATED" },
"conditions": [
{
"type": "user_attribute",
"attribute": "employeeType",
"operator": "equals",
"value": "full-time"
}
],
"steps": [
{ "type": "assign_role", "role": "employee" },
{ "type": "add_to_group", "group": "/All-Employees" }
]
}'
# List workflows
curl -s "http://localhost:8080/admin/realms/my-realm/workflows" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" | jq '.'
# Get workflow by ID
WORKFLOW_ID="workflow-uuid"
curl -s "http://localhost:8080/admin/realms/my-realm/workflows/${WORKFLOW_ID}" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" | jq '.'
# Update workflow
curl -X PUT "http://localhost:8080/admin/realms/my-realm/workflows/${WORKFLOW_ID}" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "Employee Onboarding v2",
"enabled": true,
"steps": [
{ "type": "assign_role", "role": "employee" },
{ "type": "add_to_group", "group": "/All-Employees" },
{ "type": "invoke_api", "url": "https://hr.example.com/api/provisioned", "method": "POST" }
]
}'
# Delete workflow
curl -X DELETE "http://localhost:8080/admin/realms/my-realm/workflows/${WORKFLOW_ID}" \
-H "Authorization: Bearer ${ACCESS_TOKEN}"
# Enable/Disable workflow
curl -X PUT "http://localhost:8080/admin/realms/my-realm/workflows/${WORKFLOW_ID}" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{ "enabled": false }'
7. Defining Conditions
7.1 User Attribute Conditions
{
"conditions": [
{
"type": "user_attribute",
"attribute": "department",
"operator": "equals",
"value": "engineering"
},
{
"type": "user_attribute",
"attribute": "employeeType",
"operator": "in",
"values": ["full-time", "contract"]
}
]
}
7.2 Group Membership Conditions
{
"conditions": [
{
"type": "group_membership",
"group": "/Engineering",
"operator": "is_member"
}
]
}
7.3 Time-based Conditions
{
"conditions": [
{
"type": "time",
"attribute": "user.createdTimestamp",
"operator": "older_than_days",
"value": 90
}
]
}
7.4 Event-based Conditions
{
"conditions": [
{
"type": "event",
"attribute": "event.details.updated_attribute",
"operator": "equals",
"value": "department"
}
]
}
8. Defining Steps
8.1 Assign Role
{
"type": "assign_role",
"role": "employee",
"scope": "realm"
}
// Client role
{
"type": "assign_role",
"role": "editor",
"scope": "client",
"clientId": "my-app"
}
8.2 Remove Role
{
"type": "remove_role",
"role": "contractor",
"scope": "realm"
}
8.3 Add to Group
{
"type": "add_to_group",
"group": "/Company/Engineering/Backend"
}
8.4 Remove from Group
{
"type": "remove_from_group",
"group": "/Company/Marketing"
}
8.5 Send Notification
{
"type": "send_email",
"to": "${user.email}",
"template": "welcome-employee",
"params": {
"name": "${user.firstName}",
"department": "${user.attributes.department}",
"manager": "${user.attributes.manager}"
}
}
8.6 Invoke External API
{
"type": "invoke_api",
"url": "https://hr-system.example.com/api/v1/employees/provisioned",
"method": "POST",
"headers": {
"Content-Type": "application/json",
"X-API-Key": "${env.HR_API_KEY}"
},
"body": {
"employeeId": "${user.attributes.employeeId}",
"email": "${user.email}",
"department": "${user.attributes.department}",
"provisionedAt": "${event.time}"
}
}
8.7 Set User Attribute
{
"type": "set_attribute",
"attribute": "provisionedAt",
"value": "${now()}"
}
{
"type": "set_attribute",
"attribute": "accessLevel",
"value": "standard"
}
9. Joiner-Mover-Leaver (JML) Processes
9.1 Joiner — Automated Onboarding
# workflow-joiner.yaml
name: "JML: Joiner - Employee Onboarding"
description: Auto-provision new full-time employees
enabled: true
trigger:
type: USER_CREATED
conditions:
- type: user_attribute
attribute: employeeType
operator: equals
value: full-time
steps:
# Base provisioning cho tất cả employees
- type: assign_role
role: employee
- type: add_to_group
group: /Company/All-Employees
- type: set_attribute
attribute: onboardingStatus
value: completed
- type: set_attribute
attribute: onboardedAt
value: "${now()}"
# Department-specific provisioning
- type: conditional
condition:
type: user_attribute
attribute: department
operator: equals
value: engineering
thenSteps:
- type: assign_role
role: developer
- type: add_to_group
group: /Company/Engineering
- type: assign_role
role: gitlab-user
scope: client
clientId: gitlab
- type: conditional
condition:
type: user_attribute
attribute: department
operator: equals
value: sales
thenSteps:
- type: assign_role
role: sales-rep
- type: add_to_group
group: /Company/Sales
- type: assign_role
role: crm-user
scope: client
clientId: salesforce
# Notify HR system
- type: invoke_api
url: https://hr.example.com/api/onboarding/completed
method: POST
body:
employeeId: "${user.attributes.employeeId}"
email: "${user.email}"
# Send welcome email
- type: send_email
to: "${user.email}"
template: welcome-employee
9.2 Mover — Department Transfer
# workflow-mover.yaml
name: "JML: Mover - Department Transfer"
description: Handle department changes
enabled: true
trigger:
type: USER_UPDATED
conditions:
- type: event
attribute: event.details.updated_attribute
operator: equals
value: department
steps:
# Lấy old department từ event details
# Remove old department group
- type: remove_from_group
group: "/Company/${event.details.previous_value}"
# Add to new department group
- type: add_to_group
group: "/Company/${user.attributes.department}"
# Log transfer
- type: set_attribute
attribute: lastTransferDate
value: "${now()}"
- type: set_attribute
attribute: previousDepartment
value: "${event.details.previous_value}"
# Notify managers
- type: invoke_api
url: https://hr.example.com/api/transfers
method: POST
body:
employeeId: "${user.attributes.employeeId}"
fromDepartment: "${event.details.previous_value}"
toDepartment: "${user.attributes.department}"
transferDate: "${event.time}"
9.3 Leaver — Automated Offboarding
# workflow-leaver.yaml
name: "JML: Leaver - Employee Offboarding"
description: Auto-deprovision disabled/deleted employees
enabled: true
trigger:
type: USER_DISABLED
conditions:
- type: user_attribute
attribute: employeeType
operator: in
values:
- full-time
- contract
steps:
# Revoke all active sessions
- type: invoke_api
url: "http://localhost:8080/admin/realms/my-realm/users/${user.id}/logout"
method: POST
headers:
Authorization: "Bearer ${admin.token}"
# Remove from all groups (except audit trail groups)
- type: remove_from_group
group: /Company/All-Employees
# Remove sensitive roles
- type: remove_role
role: developer
- type: remove_role
role: admin
# Set offboarding metadata
- type: set_attribute
attribute: offboardingStatus
value: completed
- type: set_attribute
attribute: offboardedAt
value: "${now()}"
- type: set_attribute
attribute: accountDisabledReason
value: employee-departure
# Notify IT and HR
- type: invoke_api
url: https://hr.example.com/api/offboarding/completed
method: POST
body:
employeeId: "${user.attributes.employeeId}"
email: "${user.email}"
offboardedAt: "${event.time}"
# Notify admin via email
- type: send_email
to: [email protected]
template: offboarding-notification
params:
employeeName: "${user.firstName} ${user.lastName}"
department: "${user.attributes.department}"
10. Access Review Workflows
Access reviews allows to periodically check whether users still need their current permissions:
# workflow-access-review.yaml
name: "Access Review: Inactive Users"
description: Review and flag inactive users
enabled: true
trigger:
type: SCHEDULED
schedule: "0 0 1 * *" # Chạy hàng tháng
conditions:
- type: user_attribute
attribute: lastLoginTimestamp
operator: older_than_days
value: 90
steps:
# Flag user for review
- type: set_attribute
attribute: accessReviewStatus
value: pending-review
- type: set_attribute
attribute: accessReviewDate
value: "${now()}"
# Add to review group
- type: add_to_group
group: /Access-Review/Pending
# Notify user's manager
- type: send_email
to: "${user.attributes.managerEmail}"
template: access-review-notification
params:
employeeName: "${user.firstName} ${user.lastName}"
lastLogin: "${user.attributes.lastLoginTimestamp}"
reviewDeadline: "${daysFromNow(14)}"
# If user has sensitive roles, escalate
- type: conditional
condition:
type: role_assigned
role: admin
thenSteps:
- type: send_email
to: [email protected]
template: access-review-escalation
params:
employeeName: "${user.firstName} ${user.lastName}"
sensitiveRoles: "${user.roles}"
11. Common Enterprise Use Cases
11.1 Contractor Lifecycle Management
# Tự động disable contractor khi hết hợp đồng
name: "Contractor: Auto-disable on contract end"
enabled: true
trigger:
type: SCHEDULED
schedule: "0 0 * * *" # Daily check
conditions:
- type: user_attribute
attribute: employeeType
operator: equals
value: contract
- type: user_attribute
attribute: contractEndDate
operator: before
value: "${now()}"
steps:
- type: disable_user
- type: set_attribute
attribute: disabledReason
value: contract-expired
- type: send_email
to: "${user.attributes.managerEmail}"
template: contractor-expired
11.2 Auto-provisioning from LDAP Sync
# Khi user được sync từ LDAP, auto-assign roles
name: "LDAP: Post-sync provisioning"
enabled: true
trigger:
type: USER_CREATED
conditions:
- type: user_attribute
attribute: LDAP_ID
operator: exists
- type: user_attribute
attribute: department
operator: exists
steps:
- type: assign_role
role: ldap-user
- type: conditional
condition:
type: user_attribute
attribute: memberOf
operator: contains
value: "CN=VPN-Users"
thenSteps:
- type: assign_role
role: vpn-access
11.3 Compliance — Password Rotation Reminder
# Nhắc nhở user đổi password sau 90 ngày
name: "Compliance: Password rotation reminder"
enabled: true
trigger:
type: SCHEDULED
schedule: "0 8 * * 1" # Weekly, Monday 8 AM
conditions:
- type: user_attribute
attribute: lastPasswordChange
operator: older_than_days
value: 80
steps:
- type: send_email
to: "${user.email}"
template: password-rotation-reminder
params:
daysRemaining: "${90 - daysBetween(user.attributes.lastPasswordChange, now())}"
- type: conditional
condition:
type: user_attribute
attribute: lastPasswordChange
operator: older_than_days
value: 90
thenSteps:
- type: set_attribute
attribute: passwordExpired
value: "true"
- type: invoke_api
url: "http://localhost:8080/admin/realms/my-realm/users/${user.id}/execute-actions-email"
method: PUT
body: ["UPDATE_PASSWORD"]
12. Monitoring and Troubleshooting
12.1 Workflow Execution Logs
# Bật debug logging cho workflows
bin/kc.sh start \
--features=workflows \
--log-level=org.keycloak.workflow:DEBUG
# Xem workflow execution history
curl -s "http://localhost:8080/admin/realms/my-realm/workflows/${WORKFLOW_ID}/executions" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" | jq '.'
# Response mẫu:
{
"executions": [
{
"id": "exec-uuid-1",
"workflowId": "workflow-uuid",
"userId": "user-uuid",
"status": "COMPLETED",
"triggeredAt": "2026-03-30T10:00:00Z",
"completedAt": "2026-03-30T10:00:02Z",
"steps": [
{ "type": "assign_role", "status": "SUCCESS" },
{ "type": "add_to_group", "status": "SUCCESS" },
{ "type": "send_email", "status": "SUCCESS" }
]
}
]
}
12.2 Common Issues
| Issue | Cause | Solution |
|---|---|---|
| Workflow not triggered | Feature not enabled or workflow disabled | Check --features=workflows and enabled: true |
| Condition does not match | Attribute name or value is wrong | Verify user attributes via Admin Console |
| Step failed | Role/Group does not exist | Create role/group before referencing in workflow |
| API call failed | External service not available | Check network connectivity, add retry logic |
| Email not sent | SMTP not configured | Configure Realm Settings → Email |
13. Best Practices
- Start simple — implement basic JML first, then add complexity
- Test workflows thoroughly before production — this is a preview feature, there may be bugs
- Idempotent steps — ensures steps can be played again without causing side effects
- Error handling — plan for external API failure or email bounce
- Audit trail — always set attributes to record workflow execution (timestamps, status)
- Separation of concerns — separate JML into separate workflows (joiner, mover, leaver)
- Version control — export workflow definitions to git, use CI/CD to deploy
- Monitor execution logs — set up alerting cho failed workflow executions
- Gradual rollout — enable workflows for small groups of users first, then expand