Iiq Zombie Task Guard
Finds IdentityIQ TaskResults stuck in "running" long enough to silently cancel the next scheduled aggregation, cert generation, or maintenance run, for the engineer who finds out a connector went…
IIQ Zombie Task Guard
Finds IdentityIQ TaskResults stuck in "running" long enough to silently cancel the next scheduled aggregation, cert generation, or maintenance run, for the engineer who finds out a connector went stale only when a certifier asks why.
Date: 2026-08-17 Type: Utility Theme: Automation + Observability Platform: SailPoint IdentityIQ Status: Idea
What it is
A Python utility that scans currently-running TaskResults, flags the ones stuck well past their task's normal duration, and checks each one against its TaskDefinition's Allow Concurrency setting. When concurrency is disabled and the next scheduled launch is close, it says so plainly: that launch is about to be silently canceled unless someone clears the stuck result first. It's read-only. It reports; it doesn't terminate anything.
Who it serves
The IAM engineer who owns the nightly aggregation and certification-generation schedule, plus whoever gets paged when an app's entitlement data turns out to be three days stale and nobody noticed.
The IIQ pain it addresses
IdentityIQ's Task Results page marks a running task "Pending" and leaves it there until it finishes, fails, or someone terminates it by hand. If the app server dies mid-run (a Tomcat restart during patching, an OOM kill, a hung connector call that never returns) the TaskResult can sit in that state indefinitely. Community threads on this are full of people describing exactly that: a result that "is just a zombie and nothing is actually running and consuming processor or memory," with no automatic recovery.
The part that turns an annoyance into an outage is Allow Concurrency. It's the common setting for aggregation and certification-generation tasks, since running two aggregations against the same application at once can corrupt account data, and IdentityIQ's own documentation is explicit about what happens when it's turned off: "the second task is canceled and an exception sent to the requestor." A zombie TaskResult counts as the first task from the scheduler's point of view. The next nightly run doesn't fail loudly. It just never happens, and nothing in the UI calls that out as different from a normal skip.
At 500+ connectors, most of them on their own daily or weekly aggregation schedule, the odds that at least one TaskResult is quietly zombied on any given morning aren't low. Nobody notices until the drift between IIQ and the source system shows up in a certification, or an auditor asks why an entitlement that was revoked last week still appears active.
How it works
- Pull running TaskResults. The utility reads every TaskResult with no completion timestamp, the same set the Task Results page shows as "Pending."
- Check elapsed time against the task's own history. Each TaskDefinition carries an average duration. A result running more than
stuck_threshold_multipliertimes that average (3x by default) gets flagged as stuck. This margin exists so a legitimately long-running full aggregation doesn't get flagged next to a delta aggregation that's stuck at ten minutes. - Cross-reference Allow Concurrency and the next scheduled launch. Stuck results get one of three severities.
HIGHmeans concurrency is disabled and the next launch is withinconcurrency_lookahead_hours(24 by default), so that run is about to be silently canceled.MEDIUMmeans concurrency is disabled but the next launch is further out: still broken, just not urgent.LOWmeans concurrency is enabled, so IdentityIQ will just append a numeric suffix and run a duplicate instead of canceling anything. - Report. Prints a summary and a findings table to stdout; writes
zombie-task-findings.jsonunless--dry-runis set. The script doesn't terminate anything itself. Clearing a flagged result is still a human call, made from the Task Results page or the IIQ Console.
What's in this folder
README.md— this filerequirements.md— functional, non-functional, and security requirementsscript.py— Python 3.10+ utility, standard library onlysample-config.json— illustrative configuration (.invalidhost, bearer token sourced from an env var name, not a literal value)sample-output.txt— stdout from four sample runs: a full run, a--dry-run, and two failure cases (missing config file, missing required key)
How to run / read it
python3 script.py --config sample-config.json
Add --dry-run to see the findings without writing zombie-task-findings.json. No dependencies to install, standard library only. task_api.mode is set to "sample" in the shipped config, so the script runs against an embedded synthetic dataset with no HTTP call or credentials involved.
Read sample-output.txt for all four runs, including what the two failure modes print to stderr and their exit codes.
Estimated impact
The bundled sample (8 running TaskResults across 8 TaskDefinitions) surfaces 5 zombies, 3 of them about to cancel a same-day launch. Run once a day, ahead of the morning aggregation window, this turns "why is the AD data three days old" from a question a certifier asks into a two-line alert someone clears before their coffee gets cold. It doesn't save hours by itself; clearing a stuck TaskResult takes thirty seconds once you know it's there. What it saves is the days-long gap between a task silently going dark and someone noticing the data drift downstream.
Why this fits an IIQ shop with 500+ connectors
Every one of those connectors has its own aggregation schedule, and most large shops disable Allow Concurrency on aggregation and cert-generation tasks on purpose, to keep two runs from corrupting the same application's data. That's the correct setting, and it's also exactly the setting that turns one stuck TaskResult into a silently skipped run. A shop with a handful of connectors might go months without hitting this. A shop with 500 is running the dice every night across hundreds of independently scheduled tasks, any one of which can zombie after a server restart nobody scheduled around a task's runtime.
Sources
- Official documentation:
- https://documentation.sailpoint.com/identityiq/help/tasks/working_with_tasks.html — confirms the Allow Concurrency option and its exact behavior: "If enabled, allow concurrency appends a numeric value to the name of the task that started second. If disabled, the second task is canceled and an exception sent to the requestor."
- https://documentation.sailpoint.com/identityiq/help/tasks/task_results_page.html — confirms "Pending" as the status shown for a currently running task, and that termination happens manually from the Task Results page
- https://documentation.sailpoint.com/identityiq/help/tasks/task_types/system_maintenance.html — confirms the Perform Maintenance task's scope: "It prunes identity snapshots, task results, and certifications, escalates orphaned work items, and performs other background maintenance tasks," used as one of the sample dataset's task definitions
- Community / current research:
- https://developer.sailpoint.com/discuss/t/ad-aggregation-task-is-stuck-and-wont-terminate/38400/3 — practitioner thread on a stuck AD aggregation task, including the description of a stuck TaskResult as "just a zombie and nothing is actually running and consuming processor or memory"
- https://developer.sailpoint.com/discuss/t/tasks-stuck-in-task-result-page/153243 — IIQ 8.4 thread on tasks stuck in "running" state indefinitely, confirming this is a recurring, current operational issue rather than a one-off report
- https://developer.sailpoint.com/docs/api/iiq/task-results-get/ — confirms the
TaskResultresource shape exposed by IdentityIQ's REST API, the interfaceload_task_results()stubs for this utility's production version
Unverified assumption: no fetched page states outright that a stuck "Pending" TaskResult is treated by the scheduler as an in-progress instance of its TaskDefinition for Allow Concurrency purposes. That's a direct reading of the documented behavior (the scheduler has no way to distinguish a genuinely running task from a zombie one), but it's inference, not a quoted claim, and is called out in metadata.md accordingly.
Requirements
Requirements — IIQ Zombie Task Guard
Purpose
Detect IdentityIQ TaskResult rows that are still marked "running" (no completed timestamp) far past their TaskDefinition's historical average duration, and flag which of those zombie results are about to silently cancel their task's next scheduled launch — because the owning TaskDefinition has Allow Concurrency disabled.
Functional requirements
- FR1: The script MUST accept a
--config <path>argument pointing to a JSON config file matching the shape ofsample-config.json. - FR2: The script MUST read
stuck_threshold_multiplier,concurrency_lookahead_hours, andoutput_pathfrom the config file and fail with a non-zero exit code and a descriptivestderrmessage if any is missing. - FR3: The script MUST treat any TaskResult with no
completedtimestamp as currently running, and MUST compute its elapsed runtime against the extract time. - FR4: A running TaskResult MUST be classified "stuck" when its elapsed runtime exceeds its TaskDefinition's
avg_duration_minutesmultiplied bystuck_threshold_multiplier. This margin exists so a legitimately long full aggregation is not flagged next to a stuck delta aggregation that normally finishes in minutes. - FR5: For each stuck result, the script MUST read the owning TaskDefinition's
allow_concurrencyflag andnext_scheduled_launch, and MUST classify severity as:HIGH—allow_concurrencyisfalseand the next scheduled launch is withinconcurrency_lookahead_hours. IdentityIQ's documented behavior is that the second launch attempt is canceled outright when concurrency is disabled, so this stuck result is about to silently swallow the next run.MEDIUM—allow_concurrencyisfalsebut the next scheduled launch is beyondconcurrency_lookahead_hours. Still a problem, not an imminent one.LOW—allow_concurrencyistrue. IdentityIQ appends a numeric suffix and runs a duplicate instead of canceling, so nothing is silently skipped, but the zombie result is still wasting a worker thread and should be cleared.
- FR6: The script MUST print a summary (results scanned, zombie count, severity breakdown) and a findings table to stdout on every run.
- FR7: Unless
--dry-runis passed, the script MUST write all findings to the JSON file named inoutput_path. - FR8: The script MUST run against the bundled synthetic dataset with no network access and no credentials, using
sample-config.jsonas-is.
Non-functional requirements
- NFR1: No third-party dependencies — standard library only (
argparse,json,dataclasses,datetime). - NFR2: Python 3.10+ (uses
X | Noneunion syntax anddataclass(frozen=True)). - NFR3: Every function signature is type-hinted.
- NFR4: The script must never write to
output_pathwhen--dry-runis set. - NFR5: Runtime for the bundled sample dataset (8 running TaskResults, 8 TaskDefinitions) must stay under 1 second.
Security requirements
- SR1: No hard-coded credentials anywhere in
script.pyorsample-config.json. - SR2:
sample-config.jsonMUST reference the API bearer token only via an environment-variable name (auth_token_env), never a literal value. - SR3: All illustrative hostnames MUST use the
.invalidTLD. - SR4: The script is read-only against IIQ. It does not call Terminate on any TaskResult and does not modify any TaskDefinition. Clearing a flagged zombie result (via the Task Results page's Terminate action, or the IIQ Console) is a human decision this script hands off to, not an action it performs — a script auto-terminating a task that turns out not to be truly dead would be worse than the problem it's solving.
Production data-source note
load_task_results() and load_task_definitions() are stubbed to return an embedded synthetic dataset when task_api.mode is "sample". In production, mode is "live" and both functions instead call IdentityIQ's REST API — GET /identityiq/rest/taskResults for running results (completed absent) and GET /identityiq/rest/taskDefinitions for each result's owning definition's allowConcurrency flag, averageDuration, and next TaskSchedule launch time. This script does not attempt a live HTTP call in the sample artifact; wiring the real calls, and their bearer-token auth, is a follow-on implementation task, not part of this idea's scope.
Out of scope
- Terminating or otherwise remediating stuck TaskResults.
- Diagnosing why a task got stuck (JVM crash mid-run, exception swallowed by a custom Rule, connector timeout, etc.) — this script only detects the symptom on the Task Results page.
- Tracking Workflow Case (BPM) states — a separate concern from Task Scheduler / TaskResult state, and already covered by a different idea in this collection.
- Cross-platform variants (Identity Security Cloud / IdentityNow schedule jobs differently and do not expose Allow Concurrency the same way).
More from IAM Ideas