Skip to main content

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:

  1. Trigger types "created", "updated", "knowledge management", "sla task" are ALL autonomous — they are ServiceNow-internal event triggers, not human actions
  2. execution_count_30d: 0 is REAL — the connector queries sys_flow_context correctly, but these flows genuinely haven't executed (likely disabled or test flows)
  3. "Run as: User who initiates" CAN be triggered by automation — ServiceNow execution context inheritance means autonomous flows can cascade
  4. Script Includes should be EXCLUDED — they are libraries, not executors (but we need to track WHAT CALLS them)
  5. 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_cache parsing
  • 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:

  1. Monitors record fields against SLA definitions
  2. Calculates breach times using task_sla table
  3. 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:

  1. Run as: System user → Runs with admin role regardless of trigger source
  2. 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:

  • runsAs should be captured as metadata, but NOT used to classify execution_mode
  • execution_mode depends 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_context table (correct — this is the execution log)
  • Filters by flow reference field (joins flow_sys_id to execution records)
  • Uses started>=cutoff to get last 30 days
  • Relies on X-Total-Count HTTP header for accurate counts (no pagination limit bias)

Is Zero Reliable?

YES, with caveats:

  1. Disabled flows — If active=false, ServiceNow won't execute them (real zero)
  2. Test/draft flows — Many organizations publish flows to prod but never activate triggers
  3. Trigger deployment gap — Flow exists in sys_hub_flow, but trigger NOT deployed to sys_hub_trigger_instance (the connector handles this via fallback path)
  4. Execution history retention — ServiceNow purges old sys_flow_context records 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: 42 and execution_count: 7 in fixtures, proving the query works when data exists

Recommendation:

  • Add a execution_history_available: boolean property to flows
  • Query sys_flow_context WITHOUT date filter to detect if any execution records exist for a flow
  • If count_all_time == 0 AND flow.active == true → flag as suspected_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:

  1. No execution trigger — They don't run on their own
  2. Pure capability, not identity — Like a credential in a vault vs. a service that uses it
  3. Noise — Every ServiceNow instance has hundreds of OOB Script Includes

Arguments for INCLUSION:

  1. Capability inventory — A Script Include with GlideRecord('sys_user') access IS a risk surface
  2. Call chain analysis — If we track "Business Rule X calls Script Include Y", we can map execution paths
  3. 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: Capability for 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:

  1. Short-term: Add a configuration flag DISCOVER_ALL_BUSINESS_RULES=true
  2. 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:

  1. 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
  2. Count flows by execution_count_30d:

    Group by execution_count_30d == 0 vs > 0
    Expected: 77 with count=0, 15 with count>0
  3. Validate Script Includes classification:

    GET /api/v1/entities?subtype=system_execution
    Confirm all have execution_mode=autonomous (current behavior)
  4. 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_category property:
    • record_event — Record create/update/delete
    • schedule — Time-based (daily, weekly, cron)
    • system_event — SLA, knowledge management, state machine
    • user_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 runsAs as 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 automation
  • execution_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_context twice:
    1. With 30-day filter → execution_count_30d
    2. Without filter → has_any_execution_history
  • If has_any_execution_history == false AND active == true → flag as suspicious

10. Final Recommendations

Immediate Actions (This Sprint)

  1. Update trigger classification — Add "created", "updated", "knowledge management", "sla task" to _AUTONOMOUS_TRIGGERS
  2. Filter Script Includes — Remove from automation inventory OR model as Capability entity type
  3. Validate production data — Developer runs API queries to confirm classification distribution

Short-Term (Next Sprint)

  1. Add execution history confidence — Distinguish "real zero" from "data unavailable"
  2. Discover all business rules — Remove script_contains filter or make it optional
  3. Document runsAs behavior — Update architecture docs to explain execution context inheritance

Long-Term (Backlog)

  1. Model Script Include call chains — Add CALLS_FUNCTION edges from flows/business rules to Script Includes
  2. Tiered discovery — Configurable filters for "all automations" vs "security-relevant only"
  3. Customer-facing trigger categories — Abstract ServiceNow trigger types into user-friendly labels

Appendix: ServiceNow Execution Context Model (Reference)

Flow Designer Run-As Modes

Run AsBehaviorSecurity Context
System userAlways runs as adminUnrestricted access
User who initiates the sessionInherits trigger source contextCan be human OR automation
Specific userRuns as named user (e.g., integration.svc)Fixed identity

Trigger Source → Execution Context Mapping

Trigger TypeTypical SourceRun-As Context
record_createDatabase insert (could be UI, API, import, or another flow)User/system that created record
schedule (daily, weekly)ServiceNow scheduler daemonSystem (admin)
service_catalogUser submitting catalog requestRequesting user
knowledge managementKB workflow state changeUser/system that updated article
sla taskSLA engine background jobSystem (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