Skip to main content

Architect Analysis: Automation Classification Decision Tree

Role: Architect Date: 2026-02-12 Scope: How to classify ServiceNow automations (autonomous execution vs. static configuration artifact) using entity properties from the live inventory


1. Hypothesis

The current allowlist approach to execution_mode classification is structurally correct but has an incomplete trigger vocabulary, which is a solvable gap, not a design flaw.

The 22 "unknown" flows represent ServiceNow trigger types that our connector's _AUTONOMOUS_TRIGGER_TYPES list has not yet encountered. These are not ambiguous cases; they are event-driven triggers with unusual names that the ServiceNow Flow Designer API surfaces through the sys_hub_trigger_instance.trigger_type field or the label_cache fallback path. The right fix is to extend the allowlist, not to redesign the classification model.

However, the deeper architectural question -- "what counts as an automation vs. a static configuration artifact" -- requires a multi-axis decision tree that goes beyond just trigger type matching. The current execution_mode + security_relevance two-property scheme captures most of the signal space, but it conflates "can this run autonomously" (a capability question) with "has this run recently" (an evidence question). These should remain orthogonal axes in a 2x2 matrix, which is what we have. The gap is in the third dimension: what authority does it carry when it runs?


2. Complete Decision Tree

Primary Axis: CAN this execute autonomously?

This is the execution_mode property. It answers: "Without a human at the keyboard, can this thing fire?"

                     Is this a Flow Designer flow?
/ \
YES NO
| |
Has triggerTypes[]? What identitySubtype?
/ | \ / | \
YES YES NO business_ scheduled_ system_
(match (no match) | rule job execution
found) | | | | |
| See 2a | AUTONOMOUS AUTONOMOUS AUTONOMOUS
Classify below UNKNOWN
by trigger
match:

autonomous_triggers -> AUTONOMOUS
operator_triggers -> OPERATOR_ASSISTED
human_triggers -> HUMAN_TRIGGERED

2a. The "no match" branch -- what to do with unrecognized trigger types

Current unrecognized triggers from the live inventory:

Trigger stringCountExample flowsVerdict
created~5"Transfer Order"AUTONOMOUS -- record lifecycle event
updated~5Various record-update flowsAUTONOMOUS -- record lifecycle event
created or updated~5Record lifecycle flowsAUTONOMOUS -- record lifecycle event
knowledge management~3"Knowledge - Approval Publish"AUTONOMOUS -- KM module event trigger
sla task~4SLA-related flowsAUTONOMOUS -- timer/SLA engine event

All 5 are event-driven triggers that fire without human intervention. The ServiceNow Flow Designer uses natural-language trigger names in its label_cache and sys_hub_trigger_instance records. "created" is equivalent to record_create. "sla task" is an SLA engine event. "knowledge management" is a Knowledge Management module event.

The correct action: add all 5 to _AUTONOMOUS_TRIGGERS as mappings, plus add prefix-based matching for "created" and "updated" variants:

# Additions to _AUTONOMOUS_TRIGGERS
_AUTONOMOUS_TRIGGERS = {
# ... existing entries ...
# Natural-language variants from label_cache fallback
"created",
"updated",
"created or updated",
# Module-specific event triggers
"knowledge management",
"sla task",
"sla",
# Future-proofing
"approval",
"import",
"metric",
}

Secondary Axis: HAS it executed recently?

This is derived from execution_count_30d and last_observed_execution_timestamp. It answers: "Is there deterministic proof this automation ran?"

    execution_count_30d > 0  AND  last_observed_execution_timestamp exists?
/ \
YES NO
| |
ACTIVELY EXECUTING NO EXECUTION EVIDENCE
(deterministic claim) (could mean: never ran, OR
no deterministic log available)

Critical caveat for the current inventory: ALL 77 internal_inventory flows have execution_count_30d: 0. This means either:

  1. None of them have actually run in 30 days (unlikely for 45 "autonomous" active flows), OR
  2. The execution data source (sys_flow_context) is not returning records for these flows

This is the single most important data quality issue. If (2), then ALL execution evidence claims for internal flows are unreliable, and the security_relevance classification based on execution_count > 0 is under-counting active flows.

Tertiary Axis: WHAT authority does it carry when it runs?

This is the security-relevant surface area. Derived from egress, binding, and data domain properties.

    has_external_egress?
/ \
YES NO
| |
egress to identity_binding_status?
what? / \
| "bound" "unlinked"
| | |
LLM/ What SP What data
External/ roles domains does
Cloud does it it touch?
| inherit? |
| | referenced_tables
| | data_domains
v v v

SECURITY-RELEVANT POTENTIALLY RELEVANT
SURFACE EXISTS (may still mutate
sensitive tables
internally)

Combined Decision Matrix

The three axes produce the actual classification:

execution_modeExecution EvidenceSecurity SurfaceClassificationAction
autonomousYES (count > 0)External egressACTIVE AUTOMATION -- PRIORITYFull evidence pack, finding generation
autonomousYES (count > 0)Internal onlyACTIVE AUTOMATION -- MONITORTrack execution patterns, check for drift
autonomousNO (count = 0)External egressDORMANT AUTHORITYWhy is this not running? Credential expired?
autonomousNO (count = 0)Bound to SPDORMANT AUTHORITYHas auth chain but no execution -- investigate
autonomousNO (count = 0)Unlinked, no egressINTERNAL INVENTORYConfiguration artifact -- low priority
operator_assistedYESExternal egressACTIVE AUTOMATION -- PRIORITYHuman-initiated but autonomous execution
operator_assistedYESInternal onlyACTIVE AUTOMATION -- MONITORTrack catalog request patterns
operator_assistedNOAnyCATALOG ARTIFACTDefined but never requested -- ignore for now
human_triggeredAnyAnyNOT AN AUTOMATIONThis is a UI action, not autonomous execution
unknownAnyAnyCLASSIFICATION GAPFix trigger mapping first

3. The "unknown" Problem: Allowlist vs. Denylist

Current approach: Explicit allowlist

_AUTONOMOUS_TRIGGERS = {"record", "schedule", "event", ...}
# Anything not in the list -> "unknown"

Alternative: Explicit denylist

_NOT_AUTONOMOUS_TRIGGERS = {"ui_action", "manual", "button_click"}
# Anything not in the list -> "autonomous" (default-open)

My recommendation: Stay with the allowlist, but expand it significantly.

Arguments for denylist (default-autonomous):

  • ServiceNow's trigger vocabulary is large and growing (Flow Designer adds new trigger types with each release)
  • Most triggers ARE autonomous; only a few are explicitly human-initiated
  • Prevents the 29% "unknown" problem from recurring with every SN upgrade

Arguments for allowlist (current approach, which I favor):

  • Determinism requires explicitness. A denylist says "we assume this is autonomous unless we know otherwise." That is an inference, not a fact. SecurityV0's core constraint is "no inference."
  • False positive cost is high. Marking a human-triggered flow as "autonomous" would generate false dormant_authority findings. CISOs lose trust in tools that cry wolf.
  • The unknown gap is finite and observable. We can see exactly which triggers are unclassified and resolve them in priority order. A denylist hides the gap.
  • Prefix matching already handles most variants. The _AUTONOMOUS_PREFIXES tuple catches record_*, schedule_*, etc. The gap is specifically module-event triggers with natural-language names.

Recommended hybrid approach: Allowlist with a structured fallback:

@staticmethod
def _classify_execution_mode(subtype: str, props: dict) -> str:
# Phase 1: Exact match (current behavior)
# Phase 2: Prefix match (current behavior)
# Phase 3: NEW -- heuristic classification with explicit flag
# If trigger contains "record" or lifecycle verbs -> "autonomous"
# Set "execution_mode_confidence": "inferred" (vs. "deterministic")
# Phase 4: Fallback -> "unknown"

The key addition is the execution_mode_confidence property. This lets us expand coverage while maintaining the determinism contract. "autonomous (inferred)" is not the same confidence level as "autonomous (deterministic)" and the evidence pack should say so.


4. Is 29% "unknown" Acceptable for Phase 1?

No. 29% unknown is not acceptable for shipping, but it IS acceptable for the current development milestone.

Here is why:

The 29% breakdown

  • 22 of 77 internal_inventory flows have execution_mode: unknown
  • ALL 22 are security_relevance: internal_inventory (no egress, no binding, no execution)
  • ALL 22 have execution_count_30d: 0
  • ALL 22 are filtered out when filter_internal_inventory=True

So in practice, if we ship with filter_internal_inventory=True, zero "unknown" automations reach the platform graph. The customer never sees them.

But that is hiding the problem

The 22 unknowns are ALL event-driven triggers that should be classified as autonomous. Misclassifying them has no security impact today (they are filtered), but it will have impact when:

  1. A flow with "knowledge management" trigger gains external egress (new HTTP action added)
  2. A flow with "created" trigger gets bound to an OAuth/SP identity
  3. A flow with "sla task" trigger starts executing (execution_count > 0)

At that point, the flow would be promoted from internal_inventory to dormant_authority or active_external, but its execution_mode would still say "unknown" -- which undermines the evidence pack's explanation.

Threshold recommendation

MetricPhase 1 GatePhase 2 Gate
% unknown in security-relevant automations0% (hard gate)0%
% unknown in internal_inventory< 30% (soft gate)< 5%
% unknown overall< 25%< 5%

Current status: PASSES Phase 1 gate (0% unknown in security-relevant), borderline on soft gate (29% unknown in internal_inventory).

Fix effort

Adding the 5 new trigger strings to _AUTONOMOUS_TRIGGERS is a 10-line code change with 5 new test cases. This should be done before shipping, not deferred. It moves the 22 unknowns to 0 unknowns.


5. Data Model Gaps

The current property set is sufficient for Phase 1 classification but has gaps that will bite us in Phase 2.

5a. Missing: execution_context (system vs. user session)

Problem: When a flow's RUNS_AS target is "User who initiates the session", we cannot distinguish between:

  • A service catalog flow where a human clicks a button (the session user is a human)
  • A flow triggered by another flow, where the "session user" is the system context
  • A scheduled flow where the "session user" is the cron scheduler identity

Proposed property:

execution_context: "system" | "user_session" | "inherited" | "unknown"

Where to get it: The sys_hub_flow.run_as field in ServiceNow. Current extraction captures this as a reference field value (user sys_id or "User who initiates the session" display value). We need to normalize it:

run_as valueexecution_context
Empty or nullinherited (inherits from parent flow or system default)
systemsystem
Specific service account sys_idsystem (configured service identity)
User who initiates the sessionuser_session
Human user sys_iduser_session

5b. Missing: trigger_source_determinism (boolean/enum)

Problem: We know WHAT triggered a flow (trigger_type) but not whether that trigger is deterministically verifiable. A record_create trigger can be verified by checking sys_hub_trigger_instance. A knowledge management trigger derived from label_cache is a best-effort parse.

Proposed property:

trigger_source_determinism: "verified" | "parsed" | "inferred"
SourceValue
sys_hub_trigger_instance record existsverified
label_cache parse successfulparsed
Prefix/heuristic match onlyinferred

This property feeds directly into execution_mode_confidence and evidence pack quality.

5c. Missing: last_modified_at on automation nodes

Problem: We track createdAt and last_observed_execution_timestamp but not when the automation's configuration was last changed. This matters because:

  • A flow created 2 years ago and never modified is a different risk than one modified yesterday
  • Configuration changes (new actions, new trigger tables) can change security_relevance without changing execution patterns

Available in ServiceNow: sys_updated_on field on sys_hub_flow. We already extract sys_updated_by but not the timestamp.

Proposed: Add lastModifiedAt to the NormalizedNode (it is already in the schema but not populated for flows).

5d. Missing: can_trigger_other_automations (boolean)

Problem: Some flows trigger other flows (subflows, flow actions). This creates transitive execution chains that are invisible to the current property model. A flow marked internal_inventory because it has no egress might trigger a subflow that DOES have egress.

Where to get it: sys_hub_action_instance records where action_type = "subflow" or action_type = "flow_logic". If any action in the flow is a subflow call, can_trigger_other_automations = true.

This is a Phase 2 property. It requires subflow resolution in the connector.

5e. Missing: status_transition_history (array)

Problem: A disabled flow that was active 30 days ago is different from a flow that has always been disabled. We classify by current status but have no temporal context.

Minimum viable version: Capture sys_updated_on when active field changes. Full version: query sys_audit for sys_hub_flow.active changes.

Summary of gaps

PropertyPriorityEffortImpact
execution_contextP0Low (normalize existing run_as)Fixes RUNS_AS semantics
trigger_source_determinismP1Low (tag during extraction)Evidence quality
lastModifiedAtP1Trivial (already in schema)Temporal analysis
can_trigger_other_automationsP2Medium (subflow resolution)Transitive chains
status_transition_historyP3High (audit log query)Temporal completeness

6. RUNS_AS Semantics: "User who initiates the session"

This is a critical classification question.

What ServiceNow means

In Flow Designer, the run_as field has three possible states:

  1. Specific user sys_id (e.g., a service account) -- the flow always executes as that identity, regardless of who triggers it. This is a deterministic execution identity.

  2. "User who initiates the session" -- the flow executes in the security context of whoever (or whatever) triggered it. This is NOT a fixed identity; it is a delegation pattern.

  3. Empty/null -- inherited from the flow's scope or the system default (which is typically "User who initiates the session").

Why this matters for classification

"User who initiates the session" creates a polymorphic execution identity:

  • If triggered by a human clicking a Service Catalog item: runs as that human
  • If triggered by a record change that was itself caused by a different automation: runs as the identity of THAT automation (or the system user)
  • If triggered by a scheduled trigger: runs as the system scheduler identity
  • If triggered by an API call: runs as the API caller's identity

This means the SAME flow can execute with DIFFERENT authority depending on HOW it was triggered. The RUNS_AS edge in our graph currently points to a single identity, which is incorrect for flows with session-user execution.

Proposed model change

For flows with run_as = "User who initiates the session":

  1. Do NOT create a RUNS_AS edge to a specific identity. The current code creates a RUNS_AS edge to a human_identity node with the display string "User who initiates the session" -- this is wrong. That string is not an identity; it is a delegation policy.

  2. Instead, set a property on the automation node:

    execution_context: "user_session"
    execution_identity: "dynamic" // vs. "static" for configured service accounts
  3. For security analysis: A flow with execution_context: user_session inherits the MAXIMUM authority of any identity that could trigger it. This means:

    • If triggered by record changes on a table that service accounts can write to: the flow can run with service account authority
    • If triggered only by Service Catalog (human click): bounded by human user permissions
  4. For identity binding: execution_identity: dynamic means identity_binding_status: unlinked -- there is no fixed identity to bind to. This is correct behavior, not a gap.

Impact on "Knowledge - Approval Publish" example

{
"display_name": "Knowledge - Approval Publish",
"execution_mode": "unknown", // should be "autonomous"
"triggerTypes": ["knowledge management"],
"RUNS_AS": "User who initiates the session"
}

This flow:

  • Triggers on Knowledge Management events (autonomous -- no human click required)
  • Runs as whoever initiated the session (dynamic identity)
  • References sys_user table (identity domain)
  • Has no external egress
  • Has no execution evidence

Correct classification:

  • execution_mode: autonomous (knowledge management is an event trigger)
  • execution_context: user_session (runs as triggering identity)
  • execution_identity: dynamic
  • identity_binding_status: unlinked (no fixed SP/OAuth binding)
  • security_relevance: internal_inventory (no egress, no binding, no execution)

This is a low-priority automation. It does not need a finding. But it DOES need to be correctly classified so that if it ever gains external egress or execution evidence, the promotion to dormant_authority or active_external happens automatically.


7. Challenge Questions for Other Roles

For Product Owner

Q: Do customers care about execution_mode or just "has it run recently"?

My position: Customers care about execution_mode but not in isolation. What they actually need is the combined classification:

  • "This automation CAN fire without anyone clicking a button AND it HAS fired 42 times in the last 30 days AND it sends HR data to an LLM endpoint" -- this is what triggers action.
  • "This automation is autonomous but dormant" -- this triggers cleanup.
  • "This automation requires a human to click a button" -- this de-prioritizes it.

execution_mode is a filter axis, not a primary sort axis. The primary sort should be security_relevance + risk_group. execution_mode helps customers understand "could this get worse without anyone noticing?"

The product implication: do NOT show execution_mode as a top-level classification in the UI. Show it as a property in the entity detail view. The top-level view should show security_relevance and risk_group.

For CISO

Q: Is the autonomous/operator/human taxonomy even relevant for security, or should we classify by access scope?

Both matter, but for different audiences:

DimensionWho caresWhy
execution_mode (autonomous/operator/human)Security OperationsDetermines blast radius velocity. An autonomous flow fires in milliseconds on any record change. An operator-assisted flow requires a human to submit a catalog request. The time-to-impact is different.
access_scope (what can it reach)Risk ManagementDetermines blast radius size. An automation that can write to sn_hr_core_profile and exfiltrate to an LLM endpoint has a larger blast radius than one that reads incident internally.
execution_evidence (has it actually run)Audit/ComplianceDetermines whether risk is theoretical or realized. A dormant automation with admin-level access is a risk. An actively executing one with admin-level access is an incident-in-progress.

The CISO answer: we need all three dimensions. But if forced to pick one for the Phase 1 dashboard headline metric, it should be security_relevance (which combines execution evidence + access scope). execution_mode is the explanatory dimension.

For Integrator (Connector Developer)

Q: Can ServiceNow tell us definitively whether a flow can fire without human intervention?

Partially. ServiceNow provides:

  1. sys_hub_trigger_instance.trigger_type -- the most reliable signal. Values like record_create, daily, service_catalog are deterministic. But the value vocabulary is not documented by ServiceNow as an exhaustive enum; it grows with platform releases and installed plugins.

  2. sys_hub_flow.label_cache -- a JSON blob with trigger metadata. Less reliable because it encodes trigger information as display labels, not machine-readable types. "Service Catalog" is parseable; "Knowledge Management" is parseable; but custom trigger types from installed apps may have arbitrary label strings.

  3. What ServiceNow does NOT provide:

    • A boolean is_autonomous field (this does not exist)
    • A definitive enumeration of all trigger_type values (the set is extensible via plugins)
    • Execution context propagation metadata (i.e., "if this flow is triggered by another flow, what identity does it run as")

Implication for the connector: We must maintain our own trigger vocabulary mapping and accept that it will be incomplete for any given ServiceNow instance. The trigger_source_determinism property proposed in section 5b makes this incompleteness explicit rather than hidden.

For Developer

Q: What would a reclassification API look like?

A reclassification API would allow operators to override the connector's classification when they have domain knowledge we do not:

PATCH /api/v1/entities/{entity_id}/classification
{
"execution_mode": "autonomous",
"execution_mode_override_reason": "Confirmed with SN admin: knowledge management trigger fires on KB article lifecycle events without human intervention",
"execution_mode_override_by": "analyst@contoso.com",
"execution_mode_override_at": "2026-02-12T14:00:00Z"
}

Design considerations:

  1. Override vs. connector sync: The override must survive the next connector sync. This means the platform needs a classification_overrides collection that takes precedence over connector-supplied values during ingestion.

  2. Audit trail: Every reclassification is an event in the temporal timeline. It should appear in the entity's history and evidence pack.

  3. Bulk reclassification: "Reclassify all flows with trigger_type 'knowledge management' as autonomous" is a common operation. The API should support filter-based bulk updates:

    POST /api/v1/entities/bulk-reclassify
    {
    "filter": {
    "properties.triggerTypes": { "$in": ["knowledge management"] },
    "properties.execution_mode": "unknown"
    },
    "set": {
    "execution_mode": "autonomous",
    "execution_mode_override_reason": "Knowledge management triggers are event-driven"
    }
    }
  4. Impact propagation: Reclassifying execution_mode does not change security_relevance (that depends on egress + execution + binding). But if the override also changes security_relevance (e.g., "actually this is internal_inventory, not dormant_authority"), the finding evaluator must re-run for affected entities.


8. Proposed Schema Changes

8a. Add to _AUTONOMOUS_TRIGGERS (connector-side, immediate)

_AUTONOMOUS_TRIGGERS = {
# Existing
"record", "schedule", "event", "data_change",
"record_create", "record_update", "record_create_or_update",
"daily", "weekly", "run_once", "repeat",
# NEW: natural-language lifecycle triggers
"created",
"updated",
"created or updated",
# NEW: module-specific event triggers
"knowledge management",
"sla task",
"sla",
# NEW: additional ServiceNow module triggers (future-proofing)
"approval",
"import",
"metric",
"inbound_email", # alias for email
}

8b. Add execution_mode_confidence property (connector-side, Phase 1)

# In _classify_execution_mode return tuple or add to props:
props["execution_mode_confidence"] = "deterministic" # exact match in allowlist
props["execution_mode_confidence"] = "inferred" # prefix match or heuristic
props["execution_mode_confidence"] = "unknown" # no match at all

This is a new property on automation nodes. The platform does not need schema changes -- it goes into properties: Record<string, unknown>.

8c. Add execution_context property (connector-side, Phase 1)

# Derived from flow.run_as during _process_flow:
if run_as is None or run_as == "":
props["execution_context"] = "inherited"
elif run_as_is_system_or_service_account(run_as):
props["execution_context"] = "system"
elif run_as == "User who initiates the session":
props["execution_context"] = "user_session"
else:
props["execution_context"] = "user_session" # specific human user = session context

8d. Add trigger_source_determinism property (connector-side, Phase 1)

# Set during flow processing based on how the trigger was discovered:
props["trigger_source_determinism"] = "verified" # from sys_hub_trigger_instance
props["trigger_source_determinism"] = "parsed" # from label_cache
props["trigger_source_determinism"] = "inferred" # from prefix/heuristic match

8e. Populate lastModifiedAt on flow nodes (connector-side, Phase 1)

# In _process_flow:
self._add_node(
...,
last_modified_at=flow.get("updated_on"), # sys_updated_on from SN API
)

Already supported by the _add_node method signature; just not being called for flows.

8f. Fix RUNS_AS semantics for session-user flows (connector-side, Phase 2)

Current behavior (wrong):

# Creates RUNS_AS edge to a human_identity node for "User who initiates the session"
run_as_node_id = self._add_node(
node_id=f"sn-user-{run_as_val}",
node_type="human_identity",
...
)
self._add_edge(edge_type="RUNS_AS", ...)

Proposed behavior:

if run_as_val == "User who initiates the session" or run_as_is_session_user(run_as_val):
# Do NOT create RUNS_AS edge -- this is not a fixed identity
props["execution_context"] = "user_session"
props["execution_identity"] = "dynamic"
else:
# Existing behavior: create RUNS_AS edge to specific identity
props["execution_context"] = "system"
props["execution_identity"] = "static"
self._add_edge(edge_type="RUNS_AS", ...)

8g. Platform-side: classification_overrides collection (Phase 2)

interface ClassificationOverride {
tenantId: string;
entityId: string; // nodeId of the entity
overrideField: string; // e.g., "execution_mode"
overrideValue: string; // e.g., "autonomous"
reason: string; // human-readable justification
overriddenBy: string; // analyst identity
overriddenAt: string; // ISO timestamp
supersededBySyncAt?: string; // set when connector re-confirms or changes
active: boolean; // false when superseded or revoked
}

This collection is consulted during ingestion: after the connector's classification is applied, any active override for that entity takes precedence. If the connector's next sync produces a DIFFERENT classification than the override, the system emits a "classification_conflict" event for human review.


Summary of Recommendations (Prioritized)

#ChangePriorityEffortImpact
1Add 5 trigger strings to _AUTONOMOUS_TRIGGERSP010 lines + 5 testsEliminates 22/22 unknowns
2Add execution_mode_confidence propertyP015 linesEvidence quality transparency
3Add execution_context propertyP120 linesFixes RUNS_AS semantics
4Populate lastModifiedAt on flowsP11 lineTemporal completeness
5Add trigger_source_determinism propertyP110 linesEvidence source provenance
6Fix RUNS_AS for session-user flowsP230 linesGraph correctness
7Reclassification APIP2API endpoint + collectionOperator override capability
8Subflow resolution (can_trigger_other_automations)P3New discovery queryTransitive chain completeness

Items 1-2 should ship before the next connector release. They are small, safe, and directly address the 29% unknown gap. Items 3-5 should ship in the same sprint. Items 6-8 are Phase 2.