Here’s a pattern we see in every organization past 10 people: the identity provider knows who’s in which team and what they should access — but the external applications don’t. Someone joins the ops group in the IDP, and then someone else has to manually add them to Datadog, update their GitHub team membership, provision their ArgoCD RBAC role, and create their PagerDuty schedule.
That “someone else” is usually you. And it’s usually manual.
JustIAM was designed to eliminate this gap. The combination of Scheduled Tasks, Event Actions, and the Go scripting engine means your IDP can be the single source of truth that actively pushes changes outward — not just a passive directory that other systems poll.
The Two Automation Models
1. Scheduled Tasks: Full Sync on a Timer
For bulk synchronization — “make the external system match what JustIAM says” — scheduled tasks run Go code on a cron schedule. They have access to the full JustIAM API, encrypted secrets, and an HTTP client with OpenTelemetry tracing.
Example: GitHub Teams Sync
package task
import (
"config"
"fetch"
"fmt"
"idp"
"secrets"
"strings"
)
func Run() (string, error) {
org := config.Get("github_org")
token := secrets.Get("GITHUB_TOKEN")
// Get all groups with "github-team:" prefix
groups := getGroupsWithPrefix("github-team:")
var synced int
for _, group := range groups {
teamSlug := strings.TrimPrefix(group.Name, "github-team:")
members := getGroupMembers(group.ID)
// GitHub API: set team membership
for _, member := range members {
user, _ := idp.GetUserByEmail(member.Email)
githubUsername := user.Attributes["github_username"]
if githubUsername == "" {
continue
}
url := fmt.Sprintf("https://api.github.com/orgs/%s/teams/%s/memberships/%s",
org, teamSlug, githubUsername)
resp, _ := fetch.Do("PUT", url, `{"role":"member"}`,
map[string]string{
"Authorization": "Bearer " + token,
"Accept": "application/vnd.github+json",
})
if resp.StatusCode == 200 {
synced++
}
}
}
return fmt.Sprintf("synced %d memberships", synced), nil
}
Run this every hour or every 6 hours. When someone is added to a github-team:backend group in JustIAM, they show up in the corresponding GitHub team on the next sync cycle.
2. Event Actions: React Instantly to Changes
For real-time responses — “a user was just added to a group, do something now” — Event Actions fire on identity events with zero delay.
Example: Provision Datadog user on group addition
Configure an Event Action:
- Trigger:
group.member_added - Condition:
group.nameequalsdatadog-users - Action: HTTP POST to Datadog’s User API
POST https://api.datadoghq.com/api/v2/users
Headers:
DD-API-KEY: {{secrets.DD_API_KEY}}
DD-APPLICATION-KEY: {{secrets.DD_APP_KEY}}
Body:
{
"data": {
"type": "users",
"attributes": {
"email": "{{.Data.user_email}}",
"name": "{{.Data.user_name}}"
}
}
}
No code. No deployment. The webhook fires within seconds of the group membership change.
Combining Both: The Full Pattern
The most powerful setups use both models together:
- Scheduled task runs every N hours as a reconciliation loop — ensuring drift is corrected even if an event was missed
- Event action fires in real-time for immediate provisioning — so users don’t wait for the next sync cycle
This gives you both speed and resilience. The event action handles the happy path (instant provisioning); the scheduled task handles edge cases (API was down, event was dropped, someone made a manual change in the external system).
Real-World Scenarios
Scenario 1: ArgoCD RBAC from JustIAM Groups
ArgoCD uses a ConfigMap or policy CSV for RBAC. You can generate it from JustIAM groups:
package task
import (
"config"
"fetch"
"fmt"
"idp"
"secrets"
"strings"
)
func Run() (string, error) {
// Build policy CSV from JustIAM groups
var policies []string
adminGroup, _ := idp.GetGroupByName("argocd-admins")
for _, member := range getGroupMembers(adminGroup.ID) {
policies = append(policies, fmt.Sprintf("p, %s, applications, *, */*, allow", member.Email))
}
viewerGroup, _ := idp.GetGroupByName("argocd-viewers")
for _, member := range getGroupMembers(viewerGroup.ID) {
policies = append(policies, fmt.Sprintf("p, %s, applications, get, */*, allow", member.Email))
}
csv := strings.Join(policies, "\n")
// Push to ArgoCD via its API or a ConfigMap update
resp, err := fetch.Do("PUT", config.Get("argocd_url") + "/api/v1/rbac",
csv, map[string]string{
"Authorization": "Bearer " + secrets.Get("ARGOCD_TOKEN"),
})
return fmt.Sprintf("pushed %d policies, status %d", len(policies), resp.StatusCode), err
}
Schedule this every 5 minutes. When someone is added to argocd-admins in JustIAM, they get admin access in ArgoCD within 5 minutes — automatically.
Scenario 2: Deactivate Users Across Systems on Offboarding
When a user is deactivated in JustIAM, fire an Event Action that calls a script task:
Event Action:
- Trigger:
user.deactivated - Action: Trigger Task →
offboard-user
Task script:
package task
import (
"config"
"fetch"
"fmt"
"secrets"
)
func Run() (string, error) {
email := config.Get("trigger_user_email")
// Disable in Datadog
fetch.Do("PATCH", "https://api.datadoghq.com/api/v2/users/" + email,
`{"data":{"attributes":{"disabled":true}}}`,
map[string]string{"DD-API-KEY": secrets.Get("DD_API_KEY")})
// Remove from PagerDuty
fetch.Do("DELETE", "https://api.pagerduty.com/users/" + email,
"", map[string]string{"Authorization": "Token " + secrets.Get("PD_TOKEN")})
// Revoke GitHub org membership
fetch.Do("DELETE",
fmt.Sprintf("https://api.github.com/orgs/%s/members/%s",
config.Get("github_org"), config.Get("trigger_github_username")),
"", map[string]string{"Authorization": "Bearer " + secrets.Get("GITHUB_TOKEN")})
return fmt.Sprintf("offboarded %s from all systems", email), nil
}
One deactivation in JustIAM triggers a cascade across your entire toolchain.
Scenario 3: Auto-Assign Groups Based on Federated Provider
When a user logs in via a specific federated provider for the first time, automatically add them to relevant groups:
Event Action:
- Trigger:
user.created - Condition: Field
user.emailends with@contractor.example.com - Action: Trigger Task →
assign-contractor-groups
The task script adds the user to contractors, limited-access, and any other default groups.
Why Not Just Use Webhooks?
You could set up external webhook consumers for all of this. Many teams do. But consider what that requires:
- A running service to receive webhooks (deployed, monitored, scaled)
- Authentication between the IDP and the consumer
- Retry logic when the consumer is down
- A way to see what fired and what failed
- Secret management for API keys to external systems
- Logging and audit trail
JustIAM bundles all of this. The scripts run inside the IDP. Secrets are encrypted at rest. Delivery history is in the admin UI. Retries happen automatically. There’s nothing to deploy, nothing to monitor externally.
Execution Infrastructure
For simple scripts, tasks run inline in the backend process. For heavier workloads or isolation requirements, JustIAM supports external agents — separate pods that receive work via gRPC and can be scaled independently. Tasks can be routed to specific agents by label (e.g., route ML inference tasks to GPU nodes).
Getting Started
- Navigate to Automation → Task Definitions → Add Custom Task
- Write your sync logic in Go
- Set a cron schedule (or trigger via Event Action)
- Add secrets (API keys, tokens) — stored encrypted
- Add config (org names, domains) — stored in plain text
- Watch the runs in Automation → Task History
Or manage everything via Terraform:
resource "justiam_scheduled_task" "github_sync" {
name = "GitHub Teams Sync"
task_type = "script"
schedule_type = "cron"
cron_expr = "0 */2 * * *"
script = file("scripts/github_sync.go")
task_config = {
github_org = "my-org"
}
secrets = {
GITHUB_TOKEN = var.github_pat
}
}
The goal isn’t to replace dedicated provisioning platforms like SCIM connectors or Okta workflows. It’s to give you the building blocks to automate what matters for your team — in code you control, running in infrastructure you own, with full visibility into what’s happening.
Scheduled Tasks documentation →
Event Actions documentation →