Automated Active Directory Attribute Sync from HRIS: Custom Playbook Implementation Guide
Overview
Employee data such as department, job title, manager, and location typically lives in your HRIS (BambooHR, Workday, Zoho People, or similar) and changes often as people are promoted, transferred, or reorganized. Active Directory (AD), which drives authentication, group membership, and many downstream tools (email, collaboration, access reviews), does not automatically pick up those changes.
This guide walks through building a custom playbook that keeps AD attributes in sync with your HRIS. The playbook reads employee changes from the HRIS and pushes the mapped attribute updates to AD through the BalkanID AD Agent's API. By the end of this guide, you will have:
A BalkanID AD Agent deployed and reachable from your playbook.
A chosen sync method (polling or webhook) configured against your HRIS.
An attribute mapping between your HRIS fields and AD attributes.
A working, tested end-to-end sync.
Solution Components
HRIS System (BambooHR, Workday, Zoho People, etc.)
System of record for employee attributes. Source of truth for department, job title, manager, location, etc.
Active Directory (AD)
Target directory where the corresponding user account attributes are updated.
BalkanID AD Agent
Runs on a jump server in API mode (not Heartbeat mode; see How It Works (Architecture), below), connected to your target AD forest(s). Exposes a REST API over HTTPS that the playbook calls to perform attribute updates.
Playbook / Sync Script
The orchestration logic that retrieves HRIS data, applies the attribute mapping, and calls the AD Agent API.
BalkanID Proxy Server
A BalkanID-operated intermediary (egress IP 147.135.94.143/32) that relays calls between a BalkanID-hosted playbook and your AD Agent. This is the IP you whitelist on your firewall when BalkanID is hosting the playbook; it does not apply if you self-host the playbook on your own workflow engine, in which case you whitelist that engine's own IP instead.
A note on flexibility: At its core, a playbook is nothing more than a workflow or script that calls the AD Agent's REST API and, for the webhook method, receives inbound calls from your HRIS. It can be built in any workflow engine you already use (n8n, Zapier, Power Automate, Tines, Temporal, etc.) or as a custom script in any programming language (Python, Node.js, Go, PowerShell, and so on). There is no requirement to use a specific tool. Sample Reference Implementation (n8n), later in this guide, walks through a sample reference implementation built in n8n, purely as an illustration.
A note on playbook ownership: If you build and host the playbook yourself, you have full visibility into its logic, hosting environment, and credentials. If instead the BalkanID team designs, builds, and/or hosts the playbook on your behalf, you may not have full visibility into the specific implementation details (exact workflow logic, hosting location, credential handling, etc.). Reach out to your BalkanID contact for more information on how your specific playbook is built and hosted.
How It Works (Architecture)
This use case relies on the BalkanID AD Agent running in API mode.
* Items marked with an asterisk are configurable and shown here with their default/recommended values. See Configure Network Access (Ports and Firewall), below, for the full port and firewall reference.
Flow description:
The playbook issues a
POST /ldap/queryrequest toward the AD Agent, by default on TCP port 5001, to search AD for the user corresponding to the employee (matching, for example, onuserPrincipalName) and to read back the specific attributes needed (such asobjectGUIDand manager references). If BalkanID is hosting the playbook, this call is relayed through BalkanID's Proxy Server (egress IP147.135.94.143/32); if you are self-hosting the playbook on your own workflow engine, it calls the AD Agent directly using your own whitelisted IP instead, with no BalkanID proxy involved.The request passes through your firewall (inbound HTTPS, restricted to whichever source IP you've whitelisted, either BalkanID's Proxy Server or your own workflow engine's IP) and is forwarded through your reverse proxy/WAF, if you use one.
The AD Agent, running on the secure jumpbox/bastion host, issues an LDAP search against your domain controller(s) (parent and/or child DCs), using LDAP on port 389 or, recommended, LDAPS on port 636, using the filter and attribute list supplied in the request body.
The DC returns the matching data to the AD Agent, which packages it into a JSON response.
The response is returned back through the reverse proxy and firewall (and, for BalkanID-hosted playbooks, back through the BalkanID Proxy Server) to the playbook, which compares it against the corresponding HRIS record to determine whether any attributes have drifted. The same lookup pattern is also used to resolve the employee's manager (see Attribute Mapping Reference, below).
When an attribute change needs to be applied, the playbook calls the AD Agent's update endpoint,
PUT /users/{objectGUID}(where{objectGUID}is the AD object's GUID returned by the lookup in step 1), back through the same inbound path on the same port, with the mapped attribute payload.The AD Agent executes the write (an LDAP modify) against the appropriate DC to update the AD user object's attributes.
Independently of the playbook flow, the AD Agent also makes its own outbound HTTPS (port 443) calls to BalkanID's SaaS endpoints. These are fixed BalkanID-hosted destinations and must be allowed on egress for the agent to function correctly.
API Endpoints Used
POST
/ldap/query
Search AD for a user (or their manager) using an LDAP filter. The response includes the object's Distinguished Name by default, plus any additional attributes explicitly requested (e.g., objectGUID, manager, managerUserPrincipalName). Used because a filter-based search requires a request body.
GET
/users/{objectGUID}
Retrieve a single AD user directly by their AD object GUID, when the GUID is already known.
PUT
/users/{objectGUID}
Update the specified AD user's attributes.
⚠️ Important: Heartbeat Mode is not supported for this use case. This attribute-sync flow currently can only be implemented using the AD Agent's API mode (steps ①-⑦ above, driven by the external playbook). It cannot currently be implemented using the AD Agent's Heartbeat mode, since heartbeat mode operates on BalkanID's own extraction/sync cadence (data pulled every 2 hours, requests processed every 10 minutes) and lifecycle actions initiated from the BalkanID platform side, rather than accepting on-demand attribute-update calls driven by an external HRIS-triggered playbook. Reach out to your BalkanID contact for more information before assuming this pattern can be adapted to Heartbeat mode.
Prerequisites
Before you begin, confirm the following and complete the setup steps below.
Planning checklist:
Confirm the exact HRIS system in use and whether it supports webhooks (needed to decide between the two sync methods; see Choosing Your Sync Method, below).
Confirm the full list of AD attributes in scope for sync and whether any require custom schema extensions.
Confirm ownership of the workflow engine/playbook build (BalkanID-hosted vs. customer-built).
Confirm your manager-attribute resolution strategy (HRIS reference format vs. AD lookup); see Attribute Mapping Reference, further down.
If polling, confirm your intended polling cadence against your HRIS API's rate limits.
Set Up the BalkanID AD Agent
Create a dedicated AD service account for the AD Agent and grant it the Lifecycle Management scopes documented in the AD Agent Configuration guide. This account is what the AD Agent binds with to read and write directory objects.
Deploy the BalkanID AD Agent on a jump server in API mode (
heartbeat_mode: falseinconfig.yaml), connected to your target AD forest(s). Refer to the AD Agent setup and configuration documentation and the On-Premise AD Agent overview for installation and forest connectivity steps.Configure TLS and credentials on the agent: a certificate/key pair (
tls_cert/tls_key) and a strong, unique API key/secret (api_key/api_secret).
Configure Network Access (Ports and Firewall)
The AD Agent's inbound REST API port (default TCP 5001) is the one setting in this flow that is explicitly configurable, via server.https_port in config.yaml. If you change it from the default, update your firewall/ingress rule and the value registered with BalkanID (or configured in your own playbook/workflow engine) to match. The agent also binds to all interfaces by default, so access control is enforced entirely by your network-level firewall rules and ACLs, not by the agent itself. Never expose this port to the open internet; restrict inbound access to the specific authorized source IP(s) only.
BalkanID Proxy Server (egress 147.135.94.143/32, used only if BalkanID hosts your playbook), or your workflow engine's own IP if you self-host the playbook
AD Agent (Jumpbox/Bastion)
HTTPS (TLS)
5001
Inbound
Yes, set via server.https_port in the agent's config.yaml
Execute real-time AD operations (read/write) through the agent's REST API
AD Agent
Active Directory Domain Controllers
LDAP
389
Outbound
Depends on your DC/network config
Directory operations when using unencrypted LDAP
AD Agent
Active Directory Domain Controllers
LDAPS
636
Outbound
Depends on your DC/network config
Secure directory operations (recommended over plain LDAP)
AD Agent
api-integrators.app.balkan.id
HTTPS
443
Outbound
No (fixed BalkanID endpoint)
GraphQL API: fetch approved access requests, obtain upload URLs
AD Agent
app.balkan.id
HTTPS
443
Outbound
No (fixed BalkanID endpoint)
REST API: request a presigned upload URL for extracted AD data
AD Agent
d3g543zyzzpcxb.cloudfront.net
HTTPS
443
Outbound
No (fixed BalkanID endpoint)
Agent auto-update channel (release manifest and signed binaries)
Whitelisting steps:
Configure ingress whitelisting on the AD Agent's REST API port:
If BalkanID is building and hosting the playbook, allow inbound HTTPS on that port from BalkanID's published source CIDR (e.g.,
147.135.94.143/32, confirm the current value with BalkanID Support) only.If you are building the playbook on your own workflow engine (e.g., Zapier, n8n, Power Automate, or a custom service), identify the static outbound IP (or IP range) of that engine/server and whitelist it instead, so it can call the Agent's API directly.
In either case, the jumpbox should only accept inbound traffic on this port from the specific authorized source(s), and it must have a public or otherwise routable IP on that port for the authorized source to reach it.
Configure egress whitelisting from the AD Agent host, allowing outbound HTTPS (port 443) to the three BalkanID SaaS endpoints above, and outbound LDAP/LDAPS (ports 389/636) to your domain controllers.
Share the jumpserver's whitelisted ingress IP with the BalkanID team so it can be registered against your integration.
Choosing Your Sync Method
There are two ways to trigger the sync: polling and webhook. The right choice depends on what your HRIS supports and your tolerance for reliability risk versus latency.
Reliability
High: not dependent on any vendor's webhook delivery guarantees; every cycle re-checks the full state
Variable and vendor-dependent; some HRIS systems (e.g., BambooHR) have shown inconsistent delivery and delays in practice
Sync latency
Bounded and predictable (depends on poll interval)
Potentially near real-time, but only if the HRIS delivers events promptly and consistently, which is not guaranteed
HRIS prerequisite
API access (read) only, supported by virtually every HRIS
Webhook support, and confidence in that HRIS vendor's delivery reliability
Inbound exposure required
No
Yes (playbook must expose an endpoint)
Change detection
Playbook actively diffs against last known state on every cycle
Entirely dependent on the HRIS reliably telling you what changed
Resilience to missed events
Self-healing: the next poll cycle picks up anything missed
Depends entirely on the HRIS's retry/delivery guarantees; a dropped or delayed webhook can go unnoticed without a separate reconciliation pass
Operational complexity
Slightly higher on the playbook side (must track state and detect deltas), but this logic is fully within your (or BalkanID's) control
Lower on the playbook side in theory, but operational risk shifts to a system (the HRIS webhook pipeline) that isn't controlled or guaranteed by BalkanID
API rate-limit exposure
Higher (repeated full or incremental pulls)
Low (event-driven, low volume per call), when events actually arrive
Note on webhook reliability: Webhook delivery reliability varies by HRIS vendor and is not something BalkanID controls. For example, customers using BambooHR's webhooks have reported inconsistent delivery, including missed events and unpredictable delays, which can undercut the near-real-time benefit this method is meant to provide. If you are considering the webhook method, validate delivery reliability with your specific HRIS first, and consider pairing it with a periodic polling-based reconciliation pass to catch anything a missed or delayed webhook would otherwise let slip through.
Continue to Polling Method, right below, if you are proceeding with polling, or skip ahead to Webhook Method, further down.
Polling Method
The playbook does not expose an endpoint or subscribe to any HRIS event. Instead, it queries the HRIS API on a fixed schedule (e.g., every 15 minutes, hourly, nightly), compares the retrieved records against the last known state, and pushes any detected changes to AD.
Sequence Diagram (Polling)
Key design notes:
The state store can be as simple as a persisted "last successful sync timestamp" (if the HRIS API supports a
modifiedSincestyle filter) or a full snapshot of the last known employee attribute set (if the HRIS API only returns full records and the playbook must diff locally).The checkpoint should only be advanced after a poll cycle completes successfully, to avoid silently skipping changes if a cycle fails partway through.
Because polling re-reads employee data on every cycle, be mindful of the HRIS API's rate limits; use incremental filters where available rather than pulling the full employee roster every time.
The per-employee loop should be idempotent: re-processing the same employee record (e.g., after a retry) should not cause duplicate or conflicting AD writes.
Setup Steps (Polling)
Complete the prerequisites above (AD service account and AD Agent deployed in API mode, ingress/egress whitelisting configured).
Obtain an API key or service account credential from your HRIS system with read access to employee records. Confirm the credential's scope covers all attributes needed for the sync (department, job title, manager, location, etc.).
Determine your polling cadence based on how quickly HR changes need to reflect in AD versus your HRIS API's rate limits (e.g., every 15 minutes, hourly, or nightly). Confirm whether your HRIS API supports an incremental/
modifiedSince-style query; if not, plan for a full-record pull with local diffing.Build a state-tracking mechanism in the playbook to record the last successful sync checkpoint (timestamp or snapshot), so each poll cycle only processes new or changed records rather than reprocessing the entire employee roster every time.
Build the attribute mapping logic in the playbook (see Attribute Mapping Reference, below, for a sample mapping).
Wire up the AD Agent API integration, calling the AD Agent's
PUT /users/{objectGUID}endpoint for each employee record with changed attributes, using the API key/secret configured on the agent. If building independently, request the AD Agent's OpenAPI specification from BalkanID Support to reference the available endpoints, request/response schemas, and authentication requirements.Advance the sync checkpoint only after a poll cycle completes successfully, to avoid skipping records if a cycle fails partway through.
Test end-to-end: modify a test employee record in the HRIS, wait for (or manually trigger) the next poll cycle, and confirm the playbook detects the change, applies the correct mapping, and the AD Agent updates the corresponding AD object. Validate behavior across multiple consecutive poll cycles, including a cycle with no changes (no-op) and a cycle with multiple simultaneous changes.
Webhook Method
The HRIS is configured to emit an event whenever a subscribed employee attribute changes. The playbook exposes an inbound endpoint that receives these events and processes the update when they arrive.
Before choosing this path: Webhook reliability varies significantly by HRIS vendor and is not something BalkanID controls. For example, customers using BambooHR's webhooks have reported inconsistent delivery, including missed events and unpredictable delays, which undermines the near-real-time benefit this method is supposed to provide. Validate reliability with your specific HRIS before committing to this path, and consider pairing it with a periodic polling-based reconciliation pass (see Error Handling, Reconciliation & Testing, further down).
Sequence Diagram (Webhook)
Key design notes:
The playbook's webhook endpoint must validate the authenticity of incoming requests (HMAC signature, shared secret header, or mutual TLS, depending on what the HRIS supports) to prevent spoofed events.
The attribute mapping logic is stateless per event; it operates only on the delta included in the webhook payload, not a full employee record diff.
All AD Agent calls flow through the trusted proxy; the AD Agent itself never accepts direct inbound calls from the playbook's origin IP.
Failures (e.g., AD Agent returns an error, user not found in AD, attribute validation failure) should be logged and, where appropriate, routed to a retry queue or alerting channel rather than silently dropped.
Setup Steps (Webhook)
Complete the prerequisites above (AD service account and AD Agent deployed in API mode, ingress/egress whitelisting configured).
Configure a webhook in your HRIS system, subscribing to the relevant employee change events (attribute updates for department, job title, manager, location, etc.). Refer to your HRIS vendor's documentation for webhook setup. If your HRIS does not support webhooks, use the Polling Method, above, instead.
If BalkanID Support is building and hosting the playbook, request the playbook's webhook endpoint URL from the BalkanID team to register in the HRIS.
Configure whatever authentication your HRIS webhook delivery supports (signing secret, bearer token, IP allowlisting) to secure the endpoint.
Build the attribute mapping logic in the playbook (see Attribute Mapping Reference, below, for a sample mapping).
Wire up the AD Agent API integration, i.e., the logic that calls the AD Agent's
PUT /users/{objectGUID}endpoint with the mapped attribute payload, using the API key/secret configured on the agent. If building independently, request the AD Agent's OpenAPI specification from BalkanID Support to reference the available endpoints, request/response schemas, and authentication requirements.Test end-to-end: trigger a test attribute change in your HRIS sandbox/staging environment (or a designated test employee record), confirm the webhook fires, confirm the playbook receives and correctly maps the event, and confirm the AD Agent applies the update to the correct AD object. Validate both successful updates and expected failure handling (e.g., invalid attribute values, employee not found in AD).
Attribute Mapping Reference
The playbook maintains a mapping between HRIS fields and the PUT /users/{objectGUID} request body. This mapping should be treated as configuration, not hardcoded logic, so it can be adjusted without code changes. This mapping is shared by both the polling and webhook methods. The mapping below reflects a real reference implementation and is meant to illustrate the pattern; the exact HRIS field names and AD attributes in scope will vary by customer.
The update payload has two parts: a small set of top-level fields, and an Attributes object containing the actual AD attributes to write. Within Attributes, each value is sent as an array of strings (e.g., "title": ["Senior Engineer"]), matching AD's multi-valued attribute convention, and only fields with a valid (non-empty) value are included; missing or blank HRIS values are simply omitted rather than sent as empty strings.
supervisorEmail
Top-level: ManagerUserPrincipalName
Passed through as-is (the manager's UPN string), alongside the resolved manager reference below.
(resolved via a separate AD lookup)
Attributes.manager
Not read directly from the HRIS. The playbook calls POST /ldap/query filtering on supervisorEmail to find the manager's AD user; the AD Agent's response includes the manager's Distinguished Name (DN) by default, and that DN is what gets sent as the value of Attributes.manager.
jobTitle
Attributes.title
Direct mapping.
department
Attributes.department
Direct mapping.
employeeNumber
Attributes.employeeID
Direct mapping.
location
Attributes.physicalDeliveryOfficeName
Direct mapping.
division
Attributes.division
Direct mapping.
employmentHistoryStatus
Attributes.employeeType
Direct mapping.
employmentHistoryStatus
Attributes.extensionAttribute1
Same HRIS field also written to a second AD attribute.
division
Attributes.extensionAttribute2
Same HRIS field also written to a second AD attribute.
hireDate
Attributes.extensionAttribute3
Direct mapping.
(static value)
Attributes.company
Not sourced from the HRIS; hardcoded to the customer's company name in this reference implementation.
location, via a lookup table
Attributes.msExchUsageLocation
Resolved through a location-to-usage-location code lookup table, not a direct field mapping.
location, via a lookup table
Attributes.co
Resolved through a location-to-country lookup table, not a direct field mapping.
Fields considered but not currently mapped in this reference implementation (left as future extension points): first name, last name, display name, work email, country code, state, and postal code.
Example request body for PUT /users/{objectGUID}:
Note on the manager attribute: AD's native
managerattribute expects a full Distinguished Name (DN), not a GUID, email, or plain string. The playbook does not need to construct that DN itself: the AD Agent's/ldap/queryresponse includes the Distinguished Name by default for any object it returns, so the manager lookup call (filtering onsupervisorEmail) already hands back a usable DN, which is submitted as-is inAttributes.manageron the subsequent update call.
Sample Reference Implementation (n8n)
As noted in the Overview at the start of this guide, a playbook can be built in any workflow engine or scripting language you prefer; the requirements are simply that it can call the AD Agent's REST API (and, for the webhook method, receive inbound HTTP calls). To make this concrete, this section walks through one such reference implementation, built in n8n, purely as an illustrative example.
This particular workflow implements the per-employee "resolve and update" logic used by the polling method's "for each changed employee" step (see the Sequence Diagram (Polling) under Polling Method, earlier in this guide). It is designed as a sub-workflow, invoked once per changed employee record by an outer polling-schedule or webhook-triggered workflow, and receives that employee's HRIS data as its input.

When Executed by Another Workflow
Execute Workflow Trigger
Entry point for this sub-workflow. It is invoked once per changed employee by the outer polling or webhook-triggered workflow, which passes in that employee's HRIS record as input.
Get Employee
HTTP Request
Retrieves or normalizes the employee's data ahead of the AD lookup.
Log Fetch Employee Error
Log (data table)
Records failures when the employee record can't be retrieved, for audit and troubleshooting.
Get AD User (LDAP Query)
HTTP Request (POST /ldap/query)
Searches AD for the user matching the employee's work email (filter on userPrincipalName), requesting the objectGUID, manager, and managerUserPrincipalName attributes back.
If User Found
IF / conditional
Branches depending on whether the AD Agent returned a matching AD user.
Log Fetch AD User Error
Log (data table)
Records cases where the AD Agent call failed, or no matching AD user was found.
Get AD User (LDAP Query) - Manager
HTTP Request (POST /ldap/query)
Searches AD for the user matching the employee's supervisorEmail (filter on userPrincipalName), requesting just the objectGUID; the response also includes the manager's Distinguished Name by default, which is what's actually used to resolve the manager reference (see Attribute Mapping Reference).
If User's Manager Found
IF / conditional
Branches depending on whether the manager's AD object was found.
Set Id & Manager
Set (manual)
Combines the employee's AD objectGUID (from the first lookup, used later as the {objectGUID} in the update call's URL) and the manager's Distinguished Name (from the second lookup, used as the value for Attributes.manager) into the working data used to build the update payload.
Create Update Payload
Code
Builds the request for the AD Agent's update call: an id (the employee's objectGUID) and a payload body, applying the HRIS-to-AD attribute mapping (see Attribute Mapping Reference). Only fields with a valid, non-empty HRIS value are included.
Log Payload Creation Error
Log (data table)
Records failures while building the update payload.
Update AD User
HTTP Request (PUT /users/{objectGUID})
Calls the AD Agent's update endpoint, using the id and payload produced by the previous node, to write the mapped attributes to the AD user object.
Log Update AD User Error
Log (data table)
Records failures returned by the AD Agent when applying the update.
Observations worth calling out:
Every failure branch in this workflow (fetch employee, fetch AD user, fetch manager, build payload, update AD user) routes to its own dedicated logging step. This is a good pattern to follow regardless of which workflow engine you use, since it directly supports the auditability practice described in Security Best Practices, below.
Several of the logging nodes in the screenshot end in an unconnected stub, meaning no further action (such as an alert to Slack/email or a retry) has been wired up yet. This is a natural extension point: decide what should happen after a failure is logged (alerting, retry queue, manual review queue, etc.).
This example only shows the per-employee resolve-and-update logic; the outer trigger (webhook receiver or polling scheduler) and the HRIS-to-AD attribute mapping configuration would typically live in a separate, calling workflow.
Security Best Practices
Network segmentation: The AD Agent should only accept inbound requests, on its configured REST API port (default TCP 5001), from the whitelisted proxy (or workflow engine) IP, with no direct public exposure of the AD Agent's API. The agent binds to all interfaces by default, so this restriction must be enforced at the firewall/ACL level, not left to the agent alone.
Credential handling: HRIS API keys, webhook signing secrets, and AD Agent API credentials should be stored in a secrets manager or your workflow engine's credential vault, not hardcoded in playbook logic.
Webhook authentication: For the webhook method, validate the authenticity of every inbound event (signature verification, shared secret, or equivalent) before acting on it.
Least privilege: The HRIS API key/service account should be scoped to read-only access on employee records; it does not need write access to the HRIS.
Error Handling, Reconciliation & Testing
Failed AD Agent calls (e.g., transient network errors, AD unavailability) should be retried with backoff before being escalated as a failure.
Employees present in the HRIS but not found in AD (e.g., new hires not yet provisioned) should be logged as skipped/pending rather than causing the sync cycle to fail entirely.
Consider running a periodic reconciliation poll (e.g., nightly) even if your primary method is webhook-based, to catch any events missed due to delivery failures, downtime, or misconfiguration. This provides a self-healing safety net.
Before going live, validate both successful updates and expected failure handling (invalid attribute values, employee not found in AD, HRIS temporarily unreachable), and confirm behavior across multiple consecutive cycles, including a cycle with no changes and a cycle with multiple simultaneous changes.
Getting Help
Reach out to your BalkanID contact for the AD Agent's OpenAPI specification if you are building the playbook independently.
Reach out to your BalkanID contact for more information on Heartbeat mode if you believe your use case may need it; as noted in How It Works (Architecture), earlier in this guide, it is not currently supported for this attribute-sync flow.
If BalkanID is building and/or hosting your playbook, reach out to your BalkanID contact for details on its implementation and hosting, since you may not have direct visibility into those specifics otherwise.

