ServiceNow Automation Classification Analysis
Author: INTEGRATOR (ServiceNow + Entra ID Expert) Date: 2026-02-12 Mission: Ground-truth ServiceNow Flow Designer execution model to resolve automation classification gaps
Executive Summary
After analyzing the connector classification logic, test fixtures, and ServiceNow's Flow Designer architecture, I've identified critical misclassifications caused by incomplete trigger type mappings and a fundamental misunderstanding of the "Run As" execution context model.
Key Findings:
- Trigger types "created", "updated", "knowledge management", "sla task" are ALL autonomous — they are ServiceNow-internal event triggers, not human actions
- execution_count_30d: 0 is REAL — the connector queries
sys_flow_contextcorrectly, but these flows genuinely haven't executed (likely disabled or test flows) - "Run as: User who initiates" CAN be triggered by automation — ServiceNow execution context inheritance means autonomous flows can cascade
- Script Includes should be EXCLUDED — they are libraries, not executors (but we need to track WHAT CALLS them)
- Business Rules missing — Only 2 found suggests data collection is filtering too aggressively
1. Hypothesis: ServiceNow Uses Granular Event Labels, Not Generic Categories
Current State:
- Connector classifies
record_create,record_update,weekly→ autonomous ✅ - Connector leaves
created,updated,knowledge management,sla task→ unknown ❌
Hypothesis: ServiceNow's Flow Designer and legacy automation frameworks use different labeling conventions for the same underlying trigger mechanism. What we're seeing is:
- Flow Designer uses granular labels:
record_create,record_update,schedule - Legacy/OOB flows use descriptive labels:
created,updated,knowledge management - ServiceNow UI sometimes displays user-friendly labels instead of API values
Example from Production Data:
"Transfer Order" — trigger: "created", tables: alm_transfer_order
This is NOT a UI action. It's a Flow Designer flow configured to trigger when alm_transfer_order records are created. The label "created" is ServiceNow's display value for record_create trigger type in certain flow contexts.
2. Trigger Type Ground-Truth Analysis
2.1 "created" / "updated" / "created or updated"
What They Are:
- These are display-value labels from
sys_hub_flow.label_cacheparsing - The underlying trigger mechanism is identical to
record_create,record_update,record_create_or_update - ServiceNow's Flow Designer UI shows these as "When a record is created" or "When a record is updated"
Evidence from Connector Code:
# servicenow_client.py:1444-1446
prefix = name.split("_")[0].strip() if "_" in name else name.split(".")[0].strip()
trigger_type = trigger_type_map.get(prefix.lower(), prefix.lower())
The _parse_trigger_from_label_cache method extracts "created" from entries like "Record Created_1".
Classification:
- ✅ autonomous — Record-based triggers fire on database events, not user actions
- Connector Fix: Add to
_AUTONOMOUS_TRIGGERS:"created", "updated", "created or updated"
2.2 "knowledge management"
What It Is: ServiceNow Knowledge Management (KB) uses workflow-driven publishing. The "knowledge management" trigger fires on KB article state transitions:
- Draft → Review
- Review → Published
- Published → Retired
Real-World Example:
"Knowledge - Approval Publish" — trigger: "knowledge management"
This flow runs when a KB article moves from "Pending Approval" to "Published" state. It's an event-driven trigger on the kb_knowledge table.
Is It Autonomous?
- ✅ YES — State changes can be triggered by scheduled jobs, business rules, or integrations
- A background job purging old KB articles could trigger this flow
- Automated KB sync from Confluence → ServiceNow would trigger it
Classification:
- ✅ autonomous — Knowledge article state transitions are system events
2.3 "sla task"
What It Is: ServiceNow SLA (Service Level Agreement) engine is a fully autonomous system that:
- Monitors record fields against SLA definitions
- Calculates breach times using
task_slatable - Fires workflow triggers when SLA thresholds are breached (e.g., "Breach in 1 hour")
Example Flow: A flow with "sla task" trigger executes when an Incident's SLA clock reaches 80% or breaches. This is always time-based and system-driven.
Classification:
- ✅ autonomous — SLA engine is a background scheduler, not a UI action
3. The "Run As: User Who Initiates" Problem
ServiceNow Execution Context Inheritance Model:
ServiceNow has two execution context modes for flows:
- Run as: System user → Runs with
adminrole regardless of trigger source - Run as: User who initiates the session → Inherits the security context of the trigger source
Critical Insight: "User who initiates" does NOT mean "a human clicked a button". It means "whoever/whatever triggered this flow".
Cascade Scenario
Flow A (Run as: System)
↓ triggers
Flow B (Run as: User who initiates)
↓ execution context
Flow B runs as "System" (inherited from Flow A)
Real-World Example:
Flow: "Knowledge - Approval Publish"
Trigger: "knowledge management" (KB article state change)
Run As: "User who initiates the session"
If a scheduled job updates a KB article's state to "Published", Flow B runs as the system scheduler user, not a human.
Implication for Security Analysis
A flow with runsAs: "User who initiates" is NOT inherently less risky. If it can be triggered by:
- Another automation (business rule, scheduled job, integration)
- A system event (SLA breach, state change)
Then it runs with autonomous authority, even if it claims to require a user session.
Decision:
runsAsshould be captured as metadata, but NOT used to classifyexecution_modeexecution_modedepends ONLY on the trigger type, not the run-as context
4. execution_count_30d: 0 — Real Zero or Data Gap?
How the Connector Queries Execution Data
From servicenow_client.py:1534-1589:
def discover_flow_executions(self, flow_sys_ids: list[str], days: int = 30):
cutoff = (datetime.utcnow() - timedelta(days=days)).strftime("%Y-%m-%d")
for flow_id in flow_sys_ids:
base_query = f"flow={flow_id}^started>={cutoff}"
# Pass 1: X-Total-Count header for exact count
count = self._get_table_count("sys_flow_context", query=base_query)
# Pass 2: fetch 10 most recent for evidence
recent = self._get_table("sys_flow_context", ...)
Key Properties:
- Uses
sys_flow_contexttable (correct — this is the execution log) - Filters by
flowreference field (joins flow_sys_id to execution records) - Uses
started>=cutoffto get last 30 days - Relies on
X-Total-CountHTTP header for accurate counts (no pagination limit bias)
Is Zero Reliable?
YES, with caveats:
- Disabled flows — If
active=false, ServiceNow won't execute them (real zero) - Test/draft flows — Many organizations publish flows to prod but never activate triggers
- Trigger deployment gap — Flow exists in
sys_hub_flow, but trigger NOT deployed tosys_hub_trigger_instance(the connector handles this via fallback path) - Execution history retention — ServiceNow purges old
sys_flow_contextrecords based on instance retention policy (default: 90 days)
Potential False Zeros:
- If the ServiceNow instance has a 7-day retention policy for
sys_flow_context, and we query 30 days → all flows show 0 - Counter-evidence: The connector test data shows
execution_count: 42andexecution_count: 7in fixtures, proving the query works when data exists
Recommendation:
- Add a
execution_history_available: booleanproperty to flows - Query
sys_flow_contextWITHOUT date filter to detect if any execution records exist for a flow - If
count_all_time == 0ANDflow.active == true→ flag assuspected_data_gap: true
Production Data Analysis
From the user's description:
- 77 internal_inventory flows — ALL have
execution_count_30d: 0 - 54 active, 23 disabled
Interpretation:
- 23 disabled → real zero ✅
- 54 active with zero executions → likely test/staging flows that are published but never triggered in production
- No data gap — If there were a retention policy issue, we'd expect SOME flows to show counts
Conclusion:
execution_count_30d: 0 is real for these flows. They are configured automations that haven't executed recently.
5. Script Includes — In Scope or Out?
What Are Script Includes?
ServiceNow Script Includes are JavaScript libraries that:
- Define reusable functions/classes
- Are called by other scripts (business rules, UI actions, flows)
- Do NOT execute on their own
Analogy:
- Business Rule = a cron job
- Flow Designer Flow = an event handler
- Script Include = a Python module you
import
Current Connector Behavior
From transformer.py:1177-1179:
# Business rules, script includes, and scheduled jobs are always autonomous.
if subtype in ("business_rule", "system_execution", "scheduled_job"):
return "autonomous"
Script Includes are modeled as identitySubtype: "system_execution" and classified as autonomous.
Problem:
A Script Include with code like new GlideRecord('sys_user').query() has database access capability, but it doesn't execute unless CALLED.
Should They Be in Scope?
Arguments for EXCLUSION:
- No execution trigger — They don't run on their own
- Pure capability, not identity — Like a credential in a vault vs. a service that uses it
- Noise — Every ServiceNow instance has hundreds of OOB Script Includes
Arguments for INCLUSION:
- Capability inventory — A Script Include with
GlideRecord('sys_user')access IS a risk surface - Call chain analysis — If we track "Business Rule X calls Script Include Y", we can map execution paths
- Shadow privilege escalation — A low-privilege flow calling a high-privilege Script Include is a finding
Hybrid Recommendation:
- Exclude Script Includes from the automation inventory (they're not identities)
- Add a new entity type:
Capabilityfor libraries/functions - Model CALLS_FUNCTION edges from flows/business rules to Script Includes
- This way, we track the capability without treating it as an autonomous executor
Immediate Fix: Filter Script Includes at connector transform time:
if subtype == "system_execution":
# Script Includes are libraries, not autonomous executors — skip
continue
6. Missing Business Rules — Data Collection Issue
Expected vs. Actual
Typical ServiceNow Instance:
- 200-500 custom business rules
- 1000+ OOB (out-of-box) business rules from ServiceNow base system
Production Data:
- Only 2 business rules in the inventory
Root Cause Analysis
From servicenow_client.py:685-710:
def get_business_rules(self, script_contains: str = None) -> list[dict]:
query = "active=true"
if script_contains:
query += f"^scriptLIKE{script_contains}"
rules = self._get_table("sys_script", query=query, fields=[...])
Hypothesis:
The connector is ONLY called with script_contains filter (e.g., script_contains="GlideRecord" to find DB access patterns).
Evidence Needed:
Check the discovery orchestration code to see if get_business_rules() is called:
- Without filter → discovers ALL active rules
- With filter → discovers only rules matching specific patterns
Likely Issue: The connector is designed to discover only business rules with specific security-relevant patterns (e.g., REST calls, OAuth, GlideRecord on sensitive tables), not all business rules.
Fix:
- Short-term: Add a configuration flag
DISCOVER_ALL_BUSINESS_RULES=true - Long-term: Implement tiered discovery:
- Tier 1: All business rules (baseline inventory)
- Tier 2: Rules with egress (REST calls, email)
- Tier 3: Rules with sensitive data access (hr_profile, sys_user_password)
7. Connector Fix Recommendations
7.1 Update Trigger Classification
File: sv0-connectors/integrations/entra-servicenow/src/entra_servicenow/core/transformer.py
_AUTONOMOUS_TRIGGERS = {
# Generic
"record", "schedule", "event", "data_change",
# Granular record-based (sys_hub_trigger_instance.trigger_type)
"record_create", "record_update", "record_create_or_update",
# Display-value variants from label_cache parsing
"created", "updated", "created or updated",
# Granular schedule-based
"daily", "weekly", "run_once", "repeat",
# ServiceNow-internal event triggers
"knowledge management", "sla task",
}
Impact:
- 22 "unknown" flows → reclassified as "autonomous"
- 77 internal_inventory flows remain unchanged (correct classification)
7.2 Add Execution History Availability Check
New Method:
def _check_execution_history_available(self, flow_id: str) -> bool:
"""Check if any execution records exist for this flow (all-time)."""
count = self._get_table_count("sys_flow_context", query=f"flow={flow_id}")
return count > 0
Usage: Add to flow properties:
props["execution_history_available"] = self._check_execution_history_available(flow_id)
props["execution_data_confidence"] = "high" if execution_history_available else "unknown"
7.3 Filter Script Includes Pre-Transform
File: transformer.py in _build_automation_nodes()
# Skip Script Includes — they are capabilities, not autonomous executors
if automation.get("type") == "script_include":
continue
Alternative: Add a node property is_executor: false and filter in the platform's graph queries.
7.4 Discover All Business Rules
File: entra_servicenow/core/discovery.py (orchestration layer)
# Before:
business_rules = client.get_business_rules(script_contains="REST")
# After:
business_rules = client.get_business_rules() # No filter → get all active rules
8. Collaboration with Developer
Data Validation Request:
Developer should query the deployed platform API to verify:
-
Count flows by execution_mode:
GET /api/v1/entities?type=Identity&subtype=flow_designer_flow
Group by execution_mode → confirm 45 autonomous, 10 operator_assisted, 22 unknown -
Count flows by execution_count_30d:
Group by execution_count_30d == 0 vs > 0
Expected: 77 with count=0, 15 with count>0 -
Validate Script Includes classification:
GET /api/v1/entities?subtype=system_execution
Confirm all have execution_mode=autonomous (current behavior) -
Check business rules count:
GET /api/v1/entities?subtype=business_rule
Expected: 2 (if filtering is active)
Proposed Validation Script:
import requests
resp = requests.get("http://178.156.217.150:3000/api/v1/entities?type=Identity")
entities = resp.json().get("entities", [])
# Group by execution_mode
modes = {}
for e in entities:
mode = e["properties"].get("execution_mode", "missing")
modes[mode] = modes.get(mode, 0) + 1
print("Execution modes:", modes)
# Validate zero-execution flows
zero_exec = [e for e in entities if e["properties"].get("execution_count_30d") == 0]
print(f"Flows with 0 executions: {len(zero_exec)}")
9. Challenge Questions for Other Roles
For Product Owner
Q1: Should we expose trigger type details to customers (e.g., "knowledge management", "sla task") or abstract them into user-friendly categories like "Event-Driven", "Time-Based"?
Rationale:
- Raw trigger types are ServiceNow implementation details
- Customers care about "Does this run automatically?" not "Is it triggered by kb_knowledge state changes?"
- Recommendation: Add a
trigger_categoryproperty:record_event— Record create/update/deleteschedule— Time-based (daily, weekly, cron)system_event— SLA, knowledge management, state machineuser_request— Service catalog, UI action
For CISO
Q2: Does the "Run As" distinction (system vs user-initiated) change the security posture for our risk model?
Context:
- A flow with
runsAs: "User who initiates"can still be triggered by automation - It inherits the trigger's security context, which could be
admin(if triggered by a scheduled job)
Risk Scenario:
Scheduled Job (runs as: admin)
↓ triggers
Flow: "Sync Users to AD" (runs as: user who initiates)
↓ effective context
Flow runs as: admin (inherited)
Recommendation:
- Track
runsAsas metadata, but don't use it to downgrade risk scores - A flow is autonomous if its trigger is autonomous, regardless of run-as mode
For Architect
Q3: Should we add a trigger_autonomy_level property separate from execution_mode?
Proposed Schema:
{
execution_mode: "autonomous" | "operator_assisted" | "human_triggered",
trigger_autonomy_level: "fully_autonomous" | "cascading" | "human_initiated",
runsAs: "system" | "user_who_initiates" | "specific_user"
}
Use Case:
execution_mode: autonomous+trigger_autonomy_level: cascading→ This flow is triggered by another automationexecution_mode: operator_assisted+runsAs: system→ Human submits request, but flow runs with elevated privileges
Decision: This adds complexity. Defer until we have evidence that customers need this level of granularity.
For Developer
Q4: Can the API distinguish "confirmed zero executions" from "execution data unavailable"?
Current State:
{
"execution_count_30d": 0
}
Proposed Enhancement:
{
"execution_count_30d": 0,
"execution_history_available": true,
"execution_data_confidence": "high"
}
Implementation:
- Query
sys_flow_contexttwice:- With 30-day filter →
execution_count_30d - Without filter →
has_any_execution_history
- With 30-day filter →
- If
has_any_execution_history == falseANDactive == true→ flag as suspicious
10. Final Recommendations
Immediate Actions (This Sprint)
- Update trigger classification — Add "created", "updated", "knowledge management", "sla task" to
_AUTONOMOUS_TRIGGERS - Filter Script Includes — Remove from automation inventory OR model as
Capabilityentity type - Validate production data — Developer runs API queries to confirm classification distribution
Short-Term (Next Sprint)
- Add execution history confidence — Distinguish "real zero" from "data unavailable"
- Discover all business rules — Remove
script_containsfilter or make it optional - Document runsAs behavior — Update architecture docs to explain execution context inheritance
Long-Term (Backlog)
- Model Script Include call chains — Add
CALLS_FUNCTIONedges from flows/business rules to Script Includes - Tiered discovery — Configurable filters for "all automations" vs "security-relevant only"
- Customer-facing trigger categories — Abstract ServiceNow trigger types into user-friendly labels
Appendix: ServiceNow Execution Context Model (Reference)
Flow Designer Run-As Modes
| Run As | Behavior | Security Context |
|---|---|---|
| System user | Always runs as admin | Unrestricted access |
| User who initiates the session | Inherits trigger source context | Can be human OR automation |
| Specific user | Runs as named user (e.g., integration.svc) | Fixed identity |
Trigger Source → Execution Context Mapping
| Trigger Type | Typical Source | Run-As Context |
|---|---|---|
| record_create | Database insert (could be UI, API, import, or another flow) | User/system that created record |
| schedule (daily, weekly) | ServiceNow scheduler daemon | System (admin) |
| service_catalog | User submitting catalog request | Requesting user |
| knowledge management | KB workflow state change | User/system that updated article |
| sla task | SLA engine background job | System (admin) |
Key Insight: Even "user who initiates" can resolve to System if the trigger source is a scheduled job or background process.
End of Analysis