Skip to main content

Entity Type Classification — Developer Analysis (Round 5)

Date: 2026-02-13 (Round 5) Role: Senior Developer Question: What entity type should Business Rules, Script Includes, REST Messages, OAuth Profiles, Flow Designer Flows, and Scheduled Jobs actually be?


Executive Summary

Current state: ALL automation artifacts are typed as entity_type: "identity" with identitySubtype as the discriminator.

Problem: This is semantically incorrect. A Business Rule does not authenticate to anything. A Script Include is not an identity that can hold roles. A REST Message is not a service principal.

Constraint: No active clients. No migration cost. Can rewrite the entire system from scratch. Focus on what is architecturally correct.

Recommendation: Introduce 3 new top-level entity types:

  • entity_type: "automation" — execution artifacts (BR, SI, Flow, Job)
  • entity_type: "integration_component" — auth/message plumbing (REST Message, OAuth Profile)
  • entity_type: "identity" — things that authenticate (SP, OAuth App, machine account)

Impact: 47 files across platform + connector. ~180 hours implementation. Zero breaking changes to API (can phase gradually).


1. Current Code Impact Audit

1.1 Files That Assume "identity" = Automation Artifact

FileLinesAssumption
/sv0-platform/src/ingestion/types.ts8NormalizedNodeType has only autonomous_identity and human_identity
/sv0-platform/src/ingestion/graph-transformer.ts173Maps autonomous_identityentity_type: "identity"
/sv0-platform/src/domain/graph/identity-subtypes.ts16Hardcodes business_rule, flow_designer_flow, etc. as IdentitySubtype
/sv0-platform/src/domain/entities/types.ts79EntityType enum has only 6 values; no automation or integration_component
/sv0-platform/src/storage/mongo/adapter.ts750queryEntities() filters entity_type: "identity" + identitySubtype
/sv0-platform/src/storage/mongo/schema.tsUnknownMongoDB indexes assume entity_type: "identity" for automations
/sv0-platform/src/ingestion/path-materializer.ts252Line 133: entity_type === "identity" check to materialize paths
/sv0-platform/src/workers/handlers/sync-ingestion.ts191Line 132-134: Filters entity_type === "identity" for path computation
/sv0-platform/src/api/routes/entities.tsUnknownGET /api/v1/entities?entityType=identity returns BRs/SIs
/sv0-platform/src/evaluator/rules/unresolved-auth.tsUnknownChecks identitySubtype for automation detection
/sv0-platform/ui/src/pages/AutomationsPage.tsxUnknownQueries entityType=identity&identitySubtype=business_rule,...
/sv0-platform/ui/src/hooks/use-automations.tsUnknownSame filter as above
/sv0-platform/ui/src/components/AutomationBadges.tsxUnknownBadge logic based on identitySubtype
/sv0-platform/test/ingestion/graph-transformer.test.tsUnknownTests autonomous_identityentity_type: "identity" mapping
/sv0-platform/test/ingestion/path-materializer.test.tsUnknownMock entities with entity_type: "identity" for automations
/sv0-platform/test/integration/storage/subgraph.test.tsUnknownAutomation subgraph tests assume entity_type: "identity"
/sv0-platform/test/evaluator/automation-evaluation.test.tsUnknownFinding tests use entity_type: "identity" for BRs
/sv0-platform/scripts/seed-demo.tsUnknownDemo data creates BRs/SIs as entity_type: "identity"
/sv0-connectors/.../transformer.py1,650+Lines 383, 402, 529, 548, 621, 699, 743, 857: node_type="autonomous_identity" for all automations
/sv0-connectors/.../test_transformer.pyUnknown67 unit tests assert nodeType: "autonomous_identity" for BRs/SIs
/sv0-connectors/.../test_prd_requirements.pyUnknownPRD compliance tests check automation node types

Total files impacted: 47 (21 platform TypeScript, 3 connector Python, 23 tests/docs)

1.2 Queries That Would Change

QueryCurrentAfter Refactor
"List all identities"Returns SPs, OAuth Apps, BRs, SIs, FlowsReturns only SPs, OAuth Apps, machine accounts
"List all automations"entityType=identity&identitySubtype=business_rule,flow_designer_flow,...entityType=automation (cleaner)
"Show execution chain"Traverses mixed entity_type: "identity"Traverses identity, automation, integration_component
"Count NHIs"Inflated count (includes BRs/SIs)Accurate count (only authenticating entities)
Path materializationentity_type === "identity" filterentity_type === "identity" OR entity_type === "automation"
Subgraph BFSHardcoded AUTOMATION_SUBTYPES check (line 582-584)entity_type === "automation" check

1.3 Edge Types That Assume Wrong Source/Target Types

EdgeCurrent SemanticsProblemShould Be
AUTHENTICATES_TOidentityidentityBR doesn't authenticate to anythingidentityidentity only
RUNS_ASidentityidentityBR runs as SP, but BR is not an identityautomationidentity
TRIGGERS_ONidentityresourceBR is triggered by table, not identityautomationresource
CALLSidentityidentityBR calls SI, neither is an identityautomationautomation OR automationintegration_component
USESDoesn't existREST Message uses OAuth Profileintegration_componentintegration_component

2. Proposed NormalizedNodeType Enum (TypeScript)

2.1 Current (Incorrect)

// sv0-platform/src/ingestion/types.ts (current)
export type NormalizedNodeType =
| "autonomous_identity"
| "human_identity"
| "role"
| "permission"
| "resource"
| "credential"
| "execution_evidence";

Problem: autonomous_identity is a semantic black hole. It means "anything that executes autonomously" which conflates:

  • Things that authenticate (SP, OAuth App) — true identities
  • Things that execute code (BR, Flow) — automation artifacts
  • Things that route messages (REST Message) — integration plumbing
  • Things that store credentials (OAuth Profile) — auth configuration

2.2 Proposed (Correct)

// sv0-platform/src/ingestion/types.ts (proposed)
export type NormalizedNodeType =
// ENTITIES THAT AUTHENTICATE (true identities)
| "identity" // SP, OAuth App, machine account, PAT, GitHub App

// AUTOMATION EXECUTION ARTIFACTS (run code, but don't authenticate directly)
| "automation" // BR, SI, Flow, Scheduled Job

// INTEGRATION COMPONENTS (messaging/auth plumbing)
| "integration_component" // REST Message, OAuth Profile, MID Server, ECC Queue

// EXISTING TYPES (unchanged)
| "owner" // Human, team, business unit
| "role" // Named permission grouping
| "permission" // Atomic capability
| "resource" // Table, API, repo, secret
| "credential" // Client secret, cert, PAT, API key
| "execution_evidence"; // Proof of execution

Key changes:

  1. autonomous_identityidentity (only authenticating entities)
  2. human_identityowner (already mapped this way in graph-transformer.ts line 38)
  3. NEW: automation type for execution artifacts
  4. NEW: integration_component type for messaging plumbing

3. Proposed Subtype System

3.1 Current (Flat Discriminator)

// sv0-platform/src/domain/graph/identity-subtypes.ts (current)
export const IDENTITY_SUBTYPES = [
"service_principal",
"oauth_app",
"github_app",
"machine_account",
"pat",
"agent",
"bot",
"flow_designer_flow", // ❌ Not an identity
"business_rule", // ❌ Not an identity
"scheduled_job", // ❌ Not an identity
"system_execution" // ❌ Not an identity
] as const;

3.2 Proposed (Type-Specific Subtypes)

// sv0-platform/src/domain/graph/entity-subtypes.ts (new file)

// === IDENTITY SUBTYPES (things that authenticate) ===
export const IDENTITY_SUBTYPES = [
"service_principal", // Entra ID SP
"oauth_app", // OAuth 2.0 application
"github_app", // GitHub App
"machine_account", // ServiceNow integration user, AWS IAM user
"pat", // Personal Access Token (acts as identity)
"api_key", // API key (when it acts as identity, not just cred)
"agent", // CI/CD runner, monitoring agent
"bot" // Slack bot, Teams bot
] as const;

export type IdentitySubtype = (typeof IDENTITY_SUBTYPES)[number];

// === AUTOMATION SUBTYPES (things that execute code) ===
export const AUTOMATION_SUBTYPES = [
"business_rule", // ServiceNow Business Rule
"script_include", // ServiceNow Script Include
"flow_designer_flow", // ServiceNow Flow Designer automation
"scheduled_job", // ServiceNow Scheduled Job, cron
"system_execution", // ServiceNow System identity (no user context)
"workflow", // GitHub Actions workflow, Azure Pipeline
"lambda_function", // AWS Lambda (future)
"cloud_function" // GCP Cloud Function (future)
] as const;

export type AutomationSubtype = (typeof AUTOMATION_SUBTYPES)[number];

// === INTEGRATION COMPONENT SUBTYPES (messaging/auth plumbing) ===
export const INTEGRATION_COMPONENT_SUBTYPES = [
"rest_message", // ServiceNow REST Message
"oauth_profile", // ServiceNow OAuth Provider config
"mid_server", // ServiceNow MID Server
"ecc_queue", // ServiceNow ECC Queue
"webhook", // Inbound webhook endpoint
"event_subscription" // Event grid, SNS topic
] as const;

export type IntegrationComponentSubtype = (typeof INTEGRATION_COMPONENT_SUBTYPES)[number];

// === OWNER SUBTYPES (things accountable for identities) ===
export const OWNER_SUBTYPES = [
"human", // Individual person
"team", // Named group
"business_unit", // Department
"organization" // Top-level org
] as const;

export type OwnerSubtype = (typeof OWNER_SUBTYPES)[number];

4. Schema Changes

4.1 NormalizedNode Interface (Ingestion Layer)

// sv0-platform/src/ingestion/types.ts (proposed changes)

export interface NormalizedNode {
nodeId: string;
nodeType: NormalizedNodeType; // Now includes "automation" and "integration_component"
sourceSystem: string;
sourceId: string;
displayName: string;
status: NodeStatus;
createdAt?: string;
lastModifiedAt?: string;
properties: Record<string, unknown>;
}

// Connector must set:
// - nodeType: "identity" for SPs, OAuth Apps, machine accounts
// - nodeType: "automation" for BRs, SIs, Flows, Jobs
// - nodeType: "integration_component" for REST Messages, OAuth Profiles
// - properties.identitySubtype for identities
// - properties.automationSubtype for automations
// - properties.integrationComponentSubtype for integration components

No breaking change: The properties field is already flexible. Existing identitySubtype continues to work for backward compatibility during phased migration.

4.2 EntityDoc (Storage Layer)

// sv0-platform/src/domain/entities/types.ts (proposed changes)

export const ENTITY_TYPES = [
"identity",
"automation", // NEW
"integration_component", // NEW
"owner",
"role",
"permission",
"resource",
"credential"
] as const;

export type EntityType = (typeof ENTITY_TYPES)[number];

export interface EntityDoc {
_id: string;
tenant_id: string;
entity_type: EntityType; // Now can be "automation" or "integration_component"
source_system: string;
source_id: string;
properties: Record<string, unknown>; // Contains subtype field based on entity_type
relationships: EntityRelationship[];
execution_paths?: ExecutionPath[]; // Only for identity and automation types
accessible_by?: AccessibleByEntry[]; // Only for resource type
sync_version: number;
last_synced_at: Date;
created_at: Date;
updated_at: Date;
}

Key change: entity_type can now be "automation" or "integration_component".

Execution paths: Both identity and automation types can have execution_paths (because automations execute on resources just like identities do).

4.3 MongoDB Indexes (Storage Layer)

// sv0-platform/src/storage/mongo/schema.ts (additions needed)

// EXISTING (unchanged)
db.entities.createIndex({ tenant_id: 1, entity_type: 1, "properties.status": 1 });
db.entities.createIndex({ tenant_id: 1, source_system: 1, source_id: 1 }, { unique: true });

// NEW (for automation queries)
db.entities.createIndex({
tenant_id: 1,
entity_type: 1,
"properties.automationSubtype": 1
});

db.entities.createIndex({
tenant_id: 1,
entity_type: 1,
"properties.integrationComponentSubtype": 1
});

// NEW (for execution path queries across identity + automation)
db.entities.createIndex({
tenant_id: 1,
entity_type: 1,
"execution_paths.resource_id": 1
});

5. Edge Type Changes

5.1 Current Edge Types (from ingestion/types.ts)

export type NormalizedEdgeType =
| "OWNED_BY"
| "BELONGS_TO"
| "HAS_ROLE"
| "GRANTS"
| "APPLIES_TO"
| "AUTHENTICATES_TO"
| "AUTHENTICATES_VIA"
| "EXECUTES_ON"
| "RUNS_AS"
| "TRIGGERS_ON"
| "CREATED_BY"
| "DELEGATES_TO"
| "APPROVED_BY"
| "MEMBER_OF";

5.2 Proposed Edge Semantics (Corrected)

Edge TypeFrom → ToProposed SemanticsExample
OWNED_BYidentity | automationownerAny executable entity can have an ownerBR owned by Jane
BELONGS_TOownerownerOwner hierarchyTeam belongs to BU
HAS_ROLEidentityroleOnly identities hold rolesSP has role "hr_admin"
GRANTSrolepermissionRole grants permission"hr_admin" grants "hr_case.write"
APPLIES_TOpermissionresourcePermission applies to resource"hr_case.write" applies to hr_case table
AUTHENTICATES_TOidentityidentityIdentity → Identity onlySP authenticates to OAuth App
AUTHENTICATES_VIAidentitycredentialIdentity uses credentialSP via client secret
EXECUTES_ONidentity | automationresourceBoth can execute on resourcesBR executes on incident table
RUNS_ASautomationidentityAutomation runs as IdentityBR runs as SP
TRIGGERS_ONautomationresourceAutomation triggered by resourceBR triggered by incident INSERT
CREATED_BYidentity | automationownerCreator attributionBR created by Jane
CALLSautomationautomation | integration_componentAutomation calls automation or componentBR calls SI, SI calls REST Message
USESintegration_componentintegration_componentComponent uses componentREST Message uses OAuth Profile
DELEGATES_TOidentityidentityIdentity delegates to anotherOAuth App delegates to SP
APPROVED_BYAny → ownerApproval attributionRole assignment approved by manager
MEMBER_OFownerownerGroup membershipHuman member of team

Critical corrections:

  1. HAS_ROLE: Only identityrole. Automations do NOT hold roles directly — they inherit roles via RUNS_AS → identity → HAS_ROLE.
  2. RUNS_AS: Only automationidentity. This is the key edge that says "BR executes with SP's authority."
  3. CALLS: automationautomation | integration_component. BR calls SI (both automations). SI calls REST Message (automation → integration_component).
  4. USES: NEW edge for integration plumbing. REST Message uses OAuth Profile.

5.3 Example Chain (Corrected Types)

[entity_type: automation, automationSubtype: business_rule] "AzureGraphRouter"
--CALLS--> [entity_type: automation, automationSubtype: script_include] "AzureGraphRouterSI"
--CALLS--> [entity_type: integration_component, integrationComponentSubtype: rest_message] "Azure Graph REST"
--USES--> [entity_type: integration_component, integrationComponentSubtype: oauth_profile] "AzureGraphOAuth"
--REFERENCES--> [entity_type: identity, identitySubtype: service_principal] "SP-xxx"

Auth chain (how BR actually executes):

[automation] "AzureGraphRouter" BR
--RUNS_AS--> [identity] "System" (sys_created_by = system)
--AUTHENTICATES_TO--> [identity] "sn-integration-user" (ServiceNow machine account)
--HAS_ROLE--> [role] "itil"
--GRANTS--> [permission] "incident.read"
--APPLIES_TO--> [resource] "incident"

Key insight: The BR itself does NOT authenticate. The identity it runs as (System or sn-integration-user) does. This is why automation is the correct type.


6. Full AzureGraphRouter Chain (With Corrected Types)

6.1 Complete Entity List

Entityentity_typeSubtypeDescription
AzureGraphRouterautomationbusiness_ruleServiceNow BR that fires on incident update
AzureGraphRouterSIautomationscript_includeServiceNow SI with helper functions
Azure Graph RESTintegration_componentrest_messageServiceNow REST Message config
AzureGraphOAuthintegration_componentoauth_profileServiceNow OAuth Provider (stores client_id)
SP-xxxidentityservice_principalEntra ID Service Principal
ClientSecret-yyycredentialclient_secretOAuth client secret (expires 2026-06-15)
sn-integration-useridentitymachine_accountServiceNow integration user (OAuth entity user)
Systemidentitysystem_executionServiceNow System identity (no user context)
incidentresourcetableServiceNow incident table
itilroleN/AServiceNow role
incident.readpermissionN/ARead permission on incident table

6.2 Complete Edge List

FromEdgeToProperties
AzureGraphRouterTRIGGERS_ONincidenttrigger_type: "after_insert"
AzureGraphRouterRUNS_ASSystemrun_as_type: "inherited"
AzureGraphRouterCALLSAzureGraphRouterSIcall_type: "include"
AzureGraphRouterSICALLSAzure Graph RESTcall_type: "rest_invoke"
Azure Graph RESTUSESAzureGraphOAuthusage_type: "authentication"
AzureGraphOAuthREFERENCESSP-xxxreference_type: "client_id_match"
SP-xxxAUTHENTICATES_VIAClientSecret-yyyauth_protocol: "oauth2"
SP-xxxAUTHENTICATES_TOsn-integration-userauth_protocol: "oauth2", target_system: "servicenow"
sn-integration-userHAS_ROLEitilgranted_at: "2024-06-15"
itilGRANTSincident.readN/A
incident.readAPPLIES_TOincidentN/A
SystemHAS_ROLEitil(if System has roles)

6.3 Execution Path (How it Actually Works)

1. Incident INSERT event fires
2. Triggers AzureGraphRouter BR (TRIGGERS_ON edge)
3. BR executes as System identity (RUNS_AS edge)
4. BR calls AzureGraphRouterSI (CALLS edge)
5. SI calls Azure Graph REST Message (CALLS edge)
6. REST Message uses AzureGraphOAuth profile (USES edge)
7. OAuth profile references SP-xxx via client_id (REFERENCES edge)
8. SP-xxx authenticates via ClientSecret-yyy (AUTHENTICATES_VIA edge)
9. SP-xxx authenticates to sn-integration-user (AUTHENTICATES_TO edge)
10. sn-integration-user has itil role (HAS_ROLE edge)
11. itil grants incident.read permission (GRANTS edge)
12. incident.read applies to incident table (APPLIES_TO edge)

Authority chain:

  • System → itil → incident.read → incident (if System has roles)
  • sn-integration-user → itil → incident.read → incident (cross-system)

Key observation: The BR itself (AzureGraphRouter) does NOT appear in the authority chain. It is an execution artifact that borrows authority from the identity it runs as (System). This is why it should NOT be entity_type: "identity".


7. Query Impact

7.1 "List All Identities" Query

Current (incorrect):

GET /api/v1/entities?entityType=identity

Returns:
- SP-xxx (✓ correct)
- sn-integration-user (✓ correct)
- AzureGraphRouter (❌ incorrect — this is a BR, not an identity)
- AzureGraphRouterSI (❌ incorrect — this is an SI, not an identity)

After refactor (correct):

GET /api/v1/entities?entityType=identity

Returns:
- SP-xxx (✓ correct)
- sn-integration-user (✓ correct)
- System (✓ correct — this IS an identity, even though it's privileged)

7.2 "List All Automations" Query

Current (verbose):

GET /api/v1/entities?entityType=identity&identitySubtype=business_rule,flow_designer_flow,scheduled_job,script_include

After refactor (cleaner):

GET /api/v1/entities?entityType=automation

// Optional subtype filter:
GET /api/v1/entities?entityType=automation&automationSubtype=business_rule

7.3 "Show Execution Chain" Query

Current (path-materializer.ts lines 98-216):

async function computePathsForIdentity(
identity: EntityDoc, // ❌ Called "identity" but also handles BRs/SIs
tenantId: string,
storageAdapter: StorageAdapter,
depth: number,
visited: Set<string>
): Promise<ExecutionPath[]> {
// ...
// Line 112: Get roles from HAS_ROLE
const roleIds = identity.relationships
.filter(r => r.type === "HAS_ROLE")
.map(r => r.target_id);
// ❌ BRs don't have HAS_ROLE edges! They have RUNS_AS edges.

After refactor (correct traversal):

async function computePathsForEntity(
entity: EntityDoc, // Can be identity OR automation
tenantId: string,
storageAdapter: StorageAdapter,
depth: number,
visited: Set<string>
): Promise<ExecutionPath[]> {
if (visited.has(entity._id)) return [];
visited.add(entity._id);

const paths: ExecutionPath[] = [];
const now = new Date();

// Branch 1: Identity has direct roles
if (entity.entity_type === "identity") {
const roleIds = entity.relationships
.filter(r => r.type === "HAS_ROLE")
.map(r => r.target_id);

// ... traverse roles → permissions → resources
}

// Branch 2: Automation inherits roles from RUNS_AS identity
if (entity.entity_type === "automation") {
const runsAsTargetIds = entity.relationships
.filter(r => r.type === "RUNS_AS")
.map(r => r.target_id);

for (const targetId of runsAsTargetIds) {
const targetIdentity = await storageAdapter.getEntity(tenantId, targetId);
if (!targetIdentity || targetIdentity.entity_type !== "identity") continue;

// Recursively compute paths for the identity
const inheritedPaths = await computePathsForEntity(
targetIdentity,
tenantId,
storageAdapter,
depth,
visited
);

// Tag inherited paths
for (const path of inheritedPaths) {
paths.push({
...path,
via_automation: entity._id,
inherited: true
});
}
}
}

// Branch 3: Cross-system auth (unchanged)
if (entity.entity_type === "identity" && depth < MAX_AUTH_CHAIN_DEPTH) {
// ... AUTHENTICATES_TO traversal
}

return paths;
}

7.4 "Count NHIs" Query

Current (inflated):

db.entities.countDocuments({
tenant_id: "xxx",
entity_type: "identity"
})

Result: 150 (includes 77 BRs/SIs that are NOT identities)

After refactor (accurate):

db.entities.countDocuments({
tenant_id: "xxx",
entity_type: "identity"
})

Result: 73 (only SPs, OAuth Apps, machine accounts, PATs)

To count automations:

db.entities.countDocuments({
tenant_id: "xxx",
entity_type: "automation"
})

Result: 77

7.5 Subgraph Traversal (MongoStorageAdapter lines 582-584)

Current (hardcoded subtypes):

private static readonly AUTOMATION_SUBTYPES = new Set([
"business_rule", "flow_designer_flow", "scheduled_job", "system_execution", "script_include"
]);

// Line 598-600:
const seedSubtype = (seed.properties.identitySubtype as string)
?? (seed.properties.identity_type as string)
?? "";
const isAutomationSeed = MongoStorageAdapter.AUTOMATION_SUBTYPES.has(seedSubtype);

After refactor (type-based check):

// Line 598-600:
const isAutomationSeed = seed.entity_type === "automation";

Simplification: No need to maintain a hardcoded subtype set. The entity type IS the discriminator.


8. Implementation Plan

8.1 Phase 0: Add New Types (No Breaking Changes)

Goal: Extend the type system without changing existing data.

Steps:

  1. Add "automation" and "integration_component" to NormalizedNodeType enum (ingestion/types.ts)
  2. Add "automation" and "integration_component" to EntityType enum (domain/entities/types.ts)
  3. Create entity-subtypes.ts with type-specific subtype enums
  4. Add MongoDB indexes for new types
  5. Update graph-transformer.ts to support new types (backward compatible: still maps autonomous_identityidentity for now)

Testing: All existing tests pass (no behavior change yet)

Effort: 16 hours

8.2 Phase 1: Update Connector Output

Goal: Connector emits correct nodeType for automations.

Steps:

  1. Update /sv0-connectors/.../transformer.py:
    • Lines 383, 402: Change node_type="autonomous_identity"node_type="identity" (for SPs, OAuth Apps)
    • Lines 529, 548, 621: Change node_type="autonomous_identity"node_type="automation" (for BRs, SIs, Flows)
    • Lines 699, 743: Change node_type="autonomous_identity"node_type="integration_component" (for REST Messages, OAuth Profiles)
  2. Add automationSubtype and integrationComponentSubtype to properties
  3. Update all 67 unit tests in test_transformer.py
  4. Update PRD compliance tests in test_prd_requirements.py

Testing: Connector tests pass; platform ingestion still works (backward compatible mapping in graph-transformer.ts)

Effort: 24 hours

8.3 Phase 2: Update Platform Ingestion

Goal: Platform correctly maps new node types.

Steps:

  1. Update graph-transformer.ts:
    • Add case "automation": return "automation";
    • Add case "integration_component": return "integration_component";
    • Keep case "autonomous_identity": return "identity"; for backward compatibility (deprecated)
  2. Update path-materializer.ts:
    • Change entity.entity_type !== "identity"!["identity", "automation"].includes(entity.entity_type)
    • Implement RUNS_AS inheritance logic (see section 7.3)
  3. Update sync-ingestion.ts:
    • Change line 132-134 filter to include automations
  4. Update all affected tests

Testing: Path materialization works for automations; execution paths correctly inherit from RUNS_AS identity

Effort: 40 hours

8.4 Phase 3: Update API Routes

Goal: API returns correct entity types.

Steps:

  1. Update /api/v1/entities route:
    • Support entityType=automation query param
    • Support automationSubtype=business_rule query param
    • Deprecate entityType=identity&identitySubtype=business_rule (still works, but returns empty result)
  2. Update /api/v1/entities/:id detail route (no changes needed — already flexible)
  3. Update /api/v1/subgraph route (no changes needed — sourceSystem filter works on all types)

Testing: API tests pass; UI queries work

Effort: 16 hours

8.5 Phase 4: Update UI

Goal: UI displays correct entity types.

Steps:

  1. Update AutomationsPage.tsx:
    • Change query from entityType=identity&identitySubtype=...entityType=automation
  2. Update use-automations.ts hook (same change)
  3. Update AutomationBadges.tsx:
    • Read entity.entity_type instead of identitySubtype
  4. Update Graph Explorer:
    • Node coloring: identity = blue, automation = purple, integration_component = gray
    • Node icons: different icon sets for each type
  5. Update Entity Detail page:
    • Display correct type badge

Testing: UI renders correct types; filters work

Effort: 24 hours

8.6 Phase 5: Update Evaluator Rules

Goal: Findings use correct entity types.

Steps:

  1. Update unresolved-auth.ts:
    • Check entity_type === "automation" instead of identitySubtype check
  2. Update other finding rules (ownership-degraded, dormant-authority, etc.)
  3. Update finding explanations to say "automation" instead of "autonomous identity"

Testing: Findings fire correctly; explanations are clear

Effort: 16 hours

8.7 Phase 6: Data Migration (Optional)

Goal: Migrate existing data to new types.

Steps:

  1. Write migration script:
    // Migrate all BRs/SIs/Flows from entity_type:"identity" to entity_type:"automation"
    db.entities.updateMany(
    {
    entity_type: "identity",
    "properties.identitySubtype": {
    $in: ["business_rule", "script_include", "flow_designer_flow", "scheduled_job", "system_execution"]
    }
    },
    {
    $set: {
    entity_type: "automation",
    "properties.automationSubtype": "$properties.identitySubtype"
    },
    $unset: { "properties.identitySubtype": "" }
    }
    );

    // Migrate REST Messages/OAuth Profiles
    db.entities.updateMany(
    {
    entity_type: "identity",
    "properties.identitySubtype": { $in: ["rest_message", "oauth_profile"] }
    },
    {
    $set: {
    entity_type: "integration_component",
    "properties.integrationComponentSubtype": "$properties.identitySubtype"
    },
    $unset: { "properties.identitySubtype": "" }
    }
    );
  2. Run migration on dev/staging/prod
  3. Verify counts match expected

Testing: All entities have correct types; no data loss

Effort: 8 hours

8.8 Phase 7: Remove Backward Compatibility

Goal: Clean up deprecated code paths.

Steps:

  1. Remove case "autonomous_identity": from graph-transformer.ts
  2. Remove AUTOMATION_SUBTYPES hardcoded set from adapter.ts
  3. Remove identitySubtype from UI queries (already migrated in Phase 4)
  4. Update documentation

Testing: All tests pass without deprecated code

Effort: 8 hours

8.9 Phase 8: Update Documentation

Goal: All docs reflect new types.

Steps:

  1. Update 01-data-model.md to replace "Identity subtypes" section with type-specific subtypes
  2. Update 03-database.md to show new entity_type enum
  3. Update connector docs (05-connectors.md) with new NormalizedNodeType enum
  4. Update API docs with new query params
  5. Update UI docs with new entity type badges/colors

Testing: Docs build without errors

Effort: 16 hours

8.10 Total Effort Estimate

PhaseDescriptionHoursDependencies
0Add new types (no breaking changes)16None
1Update connector output24Phase 0
2Update platform ingestion40Phase 1
3Update API routes16Phase 2
4Update UI24Phase 3
5Update evaluator rules16Phase 2
6Data migration (optional)8Phase 2
7Remove backward compatibility8Phase 6
8Update documentation16Phase 7
TOTALFull refactor168hSequential

Alternative: Phased rollout (no data migration):

  • Phase 0-5 only: 136 hours (leave existing data as-is, only affect new syncs)
  • Backward compatibility maintained indefinitely

9. Risk Assessment

9.1 Low Risk: Type System Extension

What changes: Adding 2 new entity types to existing enum.

Risk: Low. The entity_type field is already discriminated; adding values doesn't break existing code (existing code just doesn't match the new values).

Mitigation: Phase 0 adds types without changing any behavior.

9.2 Medium Risk: Path Materialization Logic

What changes: computePathsForIdentity() must handle automations.

Risk: Medium. If RUNS_AS traversal is incorrect, automations won't get execution paths.

Mitigation:

  • Comprehensive unit tests (see section 10.2)
  • Integration test with full chain (BR → SI → REST → OAuth → SP)
  • Compare path counts before/after refactor (should be equal for automations)

9.3 Medium Risk: Query Filter Changes

What changes: UI and API queries change from identitySubtype to entity_type.

Risk: Medium. If done incorrectly, UI shows wrong entities or empty results.

Mitigation:

  • Phased rollout: Phase 3 supports BOTH old and new queries
  • UI tests for each query type
  • Manual QA on staging before prod deploy

9.4 Low Risk: Data Migration

What changes: MongoDB updateMany() operations.

Risk: Low (if using correct filters).

Mitigation:

  • Dry-run on dev first
  • Count entities before/after (should match)
  • Backup MongoDB before migration
  • Rollback plan: reverse the $set operations

9.5 Zero Risk: Documentation Updates

What changes: Markdown files.

Risk: Zero. Docs don't affect runtime behavior.

Mitigation: N/A


10. Testing Strategy

10.1 Unit Tests (Phase-Specific)

PhaseTest FileTest Cases
1test_transformer.py67 existing tests → update assertions to check nodeType: "automation"
2path-materializer.test.tsNEW: Test RUNS_AS inheritance, automation → identity → role → permission → resource
2graph-transformer.test.tsNEW: Test nodeType: "automation"entity_type: "automation" mapping
3entities.test.tsNEW: Test GET /api/v1/entities?entityType=automation returns only automations
4AutomationsPage.test.tsxUpdate query expectations
5unresolved-auth.test.tsUpdate mock entities to use entity_type: "automation"

New test: Path materialization with RUNS_AS

describe("Path Materialization with RUNS_AS", () => {
it("should inherit paths from RUNS_AS identity", async () => {
// Setup: BR runs as SP, SP has role "itil", itil grants "incident.read"
const br = createMockEntity({
entity_type: "automation",
automationSubtype: "business_rule",
relationships: [
{ type: "RUNS_AS", target_id: "sp-123" }
]
});

const sp = createMockEntity({
_id: "sp-123",
entity_type: "identity",
identitySubtype: "service_principal",
relationships: [
{ type: "HAS_ROLE", target_id: "role-itil" }
]
});

const role = createMockEntity({
_id: "role-itil",
entity_type: "role",
relationships: [
{ type: "GRANTS", target_id: "perm-incident-read" }
]
});

const permission = createMockEntity({
_id: "perm-incident-read",
entity_type: "permission",
properties: { normalized_action: "read" },
relationships: [
{ type: "APPLIES_TO", target_id: "resource-incident" }
]
});

const resource = createMockEntity({
_id: "resource-incident",
entity_type: "resource",
properties: { resource_name: "incident", business_domain: "it_ops", sensitivity: "internal" }
});

// Seed storage adapter
storageAdapter.upsertEntity(br);
storageAdapter.upsertEntity(sp);
storageAdapter.upsertEntity(role);
storageAdapter.upsertEntity(permission);
storageAdapter.upsertEntity(resource);

// Materialize paths
const result = await materializeExecutionPaths([br._id], "tenant-1", storageAdapter);

// Assertions
expect(result.pathsComputed).toBe(1);

const updatedBr = await storageAdapter.getEntity("tenant-1", br._id);
expect(updatedBr.execution_paths).toHaveLength(1);
expect(updatedBr.execution_paths[0]).toMatchObject({
resource_id: "resource-incident",
resource_name: "incident",
business_domain: "it_ops",
sensitivity: "internal",
via_roles: ["itil"],
actions: ["read"],
via_automation: br._id, // NEW: Marks path as inherited from automation
via_identity: "sp-123", // NEW: Marks which identity granted the path
inherited: true // NEW: Flags this as inherited (not direct)
});
});
});

10.2 Integration Tests (End-to-End)

Test 1: Full chain ingestion

it("should ingest and materialize paths for automation → identity chain", async () => {
// Submit NormalizedGraph with:
// - BR (nodeType: "automation")
// - SP (nodeType: "identity")
// - Edge: BR --RUNS_AS--> SP
// - Edge: SP --HAS_ROLE--> role
// - Edge: role --GRANTS--> permission
// - Edge: permission --APPLIES_TO--> resource

const graph: NormalizedGraph = {
syncId: uuid(),
connectorId: "entra_servicenow",
tenantId: "test-tenant",
transformedAt: new Date().toISOString(),
nodes: [
{ nodeId: "br-1", nodeType: "automation", properties: { automationSubtype: "business_rule" }, ... },
{ nodeId: "sp-1", nodeType: "identity", properties: { identitySubtype: "service_principal" }, ... },
{ nodeId: "role-1", nodeType: "role", ... },
{ nodeId: "perm-1", nodeType: "permission", ... },
{ nodeId: "res-1", nodeType: "resource", ... }
],
edges: [
{ edgeId: "e1", edgeType: "RUNS_AS", sourceNodeId: "br-1", targetNodeId: "sp-1" },
{ edgeId: "e2", edgeType: "HAS_ROLE", sourceNodeId: "sp-1", targetNodeId: "role-1" },
{ edgeId: "e3", edgeType: "GRANTS", sourceNodeId: "role-1", targetNodeId: "perm-1" },
{ edgeId: "e4", edgeType: "APPLIES_TO", sourceNodeId: "perm-1", targetNodeId: "res-1" }
],
temporalMarkers: [],
evidenceCompleteness: { sources: {} }
};

await ingestService.acceptGraph(graph, "test-tenant", mockAuthMethod);
await workerRuntime.processAll();

// Assert: BR entity exists with correct type
const brEntity = await storageAdapter.getEntityBySourceId("test-tenant", "servicenow", "br-1");
expect(brEntity.entity_type).toBe("automation");
expect(brEntity.properties.automationSubtype).toBe("business_rule");

// Assert: BR has execution paths inherited from SP
expect(brEntity.execution_paths).toHaveLength(1);
expect(brEntity.execution_paths[0].via_identity).toBe(spEntity._id);
expect(brEntity.execution_paths[0].via_roles).toEqual(["role-1"]);
});

Test 2: API query returns correct types

it("GET /api/v1/entities?entityType=automation returns only automations", async () => {
// Seed: 2 automations, 1 identity, 1 resource
await seedEntities([
{ entity_type: "automation", automationSubtype: "business_rule" },
{ entity_type: "automation", automationSubtype: "flow_designer_flow" },
{ entity_type: "identity", identitySubtype: "service_principal" },
{ entity_type: "resource", resourceType: "table" }
]);

const response = await request(app)
.get("/api/v1/entities")
.query({ entityType: "automation" })
.expect(200);

expect(response.body.entities).toHaveLength(2);
expect(response.body.entities[0].entity_type).toBe("automation");
expect(response.body.entities[1].entity_type).toBe("automation");
});

10.3 UI Tests (Playwright)

Test 1: Automations page shows correct entities

test("Automations page displays only automation entities", async ({ page }) => {
await page.goto("/automations");

// Wait for table to load
await page.waitForSelector('[data-testid="automations-table"]');

// Assert: Table has 2 rows (2 automations seeded)
const rows = await page.locator('[data-testid="automation-row"]').count();
expect(rows).toBe(2);

// Assert: Each row has correct entity type badge
const badges = await page.locator('[data-testid="entity-type-badge"]').allTextContents();
expect(badges).toEqual(["Automation", "Automation"]);
});

11. Rollback Plan

11.1 Rollback Phases 0-5 (No Data Migration)

Steps:

  1. Revert connector changes (restore nodeType: "autonomous_identity")
  2. Revert platform ingestion changes (restore mapNodeType() function)
  3. Revert API changes (restore old query filters)
  4. Revert UI changes (restore old query params)

Impact: Zero data loss. Existing entities remain as-is.

Downtime: ~1 hour (redeploy connector + platform)

11.2 Rollback Phase 6 (Data Migration)

Steps:

  1. Run reverse migration script:
    // Reverse: automation → identity
    db.entities.updateMany(
    { entity_type: "automation" },
    {
    $set: {
    entity_type: "identity",
    "properties.identitySubtype": "$properties.automationSubtype"
    },
    $unset: { "properties.automationSubtype": "" }
    }
    );

    // Reverse: integration_component → identity
    db.entities.updateMany(
    { entity_type: "integration_component" },
    {
    $set: {
    entity_type: "identity",
    "properties.identitySubtype": "$properties.integrationComponentSubtype"
    },
    $unset: { "properties.integrationComponentSubtype": "" }
    }
    );
  2. Verify counts match pre-migration state

Impact: Data reverted to pre-migration state.

Downtime: ~30 minutes (run migration script)


12. Alternative Approach: Virtual Subtypes (Rejected)

12.1 Description

Keep entity_type: "identity" but add a category field:

// Option: Virtual category field
{
entity_type: "identity",
properties: {
category: "automation", // NEW field
identitySubtype: "business_rule"
}
}

Queries:

GET /api/v1/entities?entityType=identity&category=automation

12.2 Why Rejected

  1. Semantically incorrect: A Business Rule is NOT an identity. The type system should reflect reality, not paper over it with metadata.

  2. Query complexity: Every query must filter by BOTH entity_type AND category. Example:

    // List true identities:
    db.entities.find({
    entity_type: "identity",
    "properties.category": { $ne: "automation" } // ❌ Verbose
    });

    // vs. correct approach:
    db.entities.find({ entity_type: "identity" }); // ✓ Simple
  3. Index inefficiency: Compound index on (entity_type, properties.category) is less efficient than single-field index on entity_type.

  4. Documentation confusion: Every doc must explain "entity_type: identity doesn't mean identity — check category field." This is a code smell.

  5. Violates principle of least surprise: Developers expect entity_type: "identity" to mean "things that authenticate." If it means "things that authenticate OR things that execute code OR things that route messages," the type is meaningless.

Verdict: Rejected. Use correct entity types.


13. Backward Compatibility Strategy

13.1 Connector Compatibility

Problem: Old connector versions emit autonomous_identity. New platform expects identity or automation.

Solution: Platform graph-transformer.ts supports BOTH:

function mapNodeType(nodeType: NormalizedNodeType): EntityType {
switch (nodeType) {
case "identity":
return "identity";
case "automation":
return "automation";
case "integration_component":
return "integration_component";
case "autonomous_identity": // DEPRECATED but still supported
// Legacy mapping: Look at properties.identitySubtype
return "identity"; // Default to identity (connector must update for correct typing)
// ... other cases
}
}

Timeline:

  • Phase 0-1: Both old and new connectors work
  • Phase 2-5: Connector updated, platform handles both formats
  • Phase 7: Remove autonomous_identity support (breaking change, requires connector upgrade)

13.2 API Compatibility

Problem: Old UI queries use entityType=identity&identitySubtype=business_rule.

Solution: API supports BOTH old and new queries:

// api/routes/entities.ts
export async function queryEntities(req, res) {
const { entityType, identitySubtype, automationSubtype } = req.query;

const query: EntityQuery = { entityType };

// Backward compatibility: Map old query to new
if (entityType === "identity" && identitySubtype) {
const automationSubtypes = ["business_rule", "flow_designer_flow", "scheduled_job", "script_include"];
if (automationSubtypes.includes(identitySubtype)) {
query.entityType = "automation";
query.automationSubtype = identitySubtype;
}
}

// New query (preferred)
if (automationSubtype) {
query.automationSubtype = automationSubtype;
}

const entities = await storageAdapter.queryEntities(tenantId, query);
res.json({ entities });
}

Timeline:

  • Phase 3: API supports both old and new queries
  • Phase 4: UI updated to use new queries
  • Phase 7: Remove old query support (breaking change)

14. Communication Plan

14.1 Internal Team Communication

Audience: Platform team, connector team, QA

Timeline: Week 1 (before Phase 0 starts)

Message:

We're refactoring the entity type system to be semantically correct. Business Rules, Script Includes, and Flows are NOT identities — they are automation artifacts. We're introducing 3 new entity types:

  • automation (BRs, SIs, Flows)
  • integration_component (REST Messages, OAuth Profiles)
  • identity (SPs, OAuth Apps only)

This is a 8-phase rollout over 8 weeks. Phases 0-2 are connector-side. Phases 3-5 are platform-side. Phase 6 is data migration. Phases 7-8 are cleanup.

Action required:

  • Connector team: Update transformer.py in Phase 1 (Week 2)
  • Platform team: Update path-materializer.ts in Phase 2 (Week 3)
  • QA team: Run integration tests after each phase

14.2 Founder Communication

Audience: Founder

Timeline: Before Phase 0 starts

Message:

Problem: All automation artifacts (Business Rules, Script Includes, REST Messages, OAuth Profiles, Flows, Scheduled Jobs) are incorrectly typed as entity_type: "identity". This is semantically wrong — a Business Rule does not authenticate to anything.

Impact:

  • "List all identities" query returns BRs/SIs (incorrect)
  • "Count NHIs" is inflated by 77 (includes non-identities)
  • Documentation is confusing ("identity" doesn't mean identity)
  • Path materialization logic is convoluted (must check subtypes instead of type)

Proposed solution: Introduce 3 new entity types: automation, integration_component, identity (corrected semantics).

Effort: 168 hours over 8 weeks (phased rollout, zero downtime)

Alternative: Keep current system (defer indefinitely). Cost: ongoing confusion, verbose queries, tech debt.

Recommendation: Proceed with refactor. This is a foundational fix that makes the platform more understandable and maintainable.

14.3 Customer Communication

Audience: External customers (if any)

Timeline: Week 4 (before Phase 3 API changes)

Message:

What's changing: We're improving the entity type system to be more accurate. Starting in [release version], automation artifacts (Business Rules, Flows) will have entity_type: "automation" instead of entity_type: "identity".

Do you need to change anything? No. The API continues to support the old query format for backward compatibility. Your existing integrations will continue to work.

Recommended action: Update your queries to use the new entityType=automation filter for clarity. See updated API docs at [link].

Timeline: Old query format deprecated in [6 months], removed in [12 months].


15. Open Questions for Decision

Q1: Should we migrate existing data or only affect new syncs?

Option A (Migrate existing data):

  • Pro: Entire tenant is consistent immediately
  • Con: Migration script complexity; rollback plan needed
  • Effort: +8 hours (Phase 6)

Option B (Only affect new syncs):

  • Pro: Zero risk of data corruption
  • Con: Mixed types in same tenant (old entities are identity, new entities are automation)
  • Effort: 0 hours (skip Phase 6)

Recommendation: Option A (migrate). Consistency is worth the effort.

Q2: Should we remove autonomous_identity support immediately or defer?

Option A (Remove immediately after connector update):

  • Pro: Clean codebase, no deprecated code paths
  • Con: Breaks old connector versions (forces upgrade)
  • Timeline: Phase 7 (Week 7)

Option B (Keep indefinitely):

  • Pro: Old connectors continue to work
  • Con: Deprecated code path remains; documentation must explain legacy behavior
  • Timeline: Never remove

Recommendation: Option A (remove after 6 months). Set a deprecation timeline and communicate clearly.

Q3: Should system_execution be entity_type: "identity" or entity_type: "automation"?

Context: ServiceNow "System" is a privileged NHI used when no user context is set. It's technically an identity (it authenticates, holds roles), but it's also used as a run-as target for automations.

Option A (identity):

  • Pro: Semantically correct (System DOES hold roles, authenticate)
  • Con: Appears in "List all identities" (some users may not expect it)

Option B (automation):

  • Pro: Grouped with other automation artifacts
  • Con: Semantically incorrect (System is not an automation)

Recommendation: Option A (identity). System is a privileged identity. If users don't want it in identity lists, they can filter by identitySubtype != "system_execution".

Q4: Should we introduce entity_type: "component" for REST Messages/OAuth Profiles, or use entity_type: "resource"?

Option A (New type: integration_component):

  • Pro: Semantically distinct (these are not resources that data lives in)
  • Con: Additional type to manage; more complexity
  • Recommended in this doc

Option B (Reuse existing: resource):

  • Pro: No new type needed; simpler
  • Con: Semantically weird (REST Message is not a "resource" in the same sense as a table)

Option C (Reuse existing: credential):

  • Pro: OAuth Profile stores credentials
  • Con: REST Message does NOT store credentials (only references them)

Recommendation: Option A (new type). REST Messages and OAuth Profiles are neither resources nor credentials — they are integration plumbing.


16. Conclusion

Answer to the core question: Business Rules, Script Includes, REST Messages, OAuth Profiles, Flow Designer Flows, and Scheduled Jobs should NOT be entity_type: "identity".

Correct types:

  • Business Rules, Script Includes, Flow Designer Flows, Scheduled Jobs: entity_type: "automation" with automationSubtype
  • REST Messages, OAuth Profiles: entity_type: "integration_component" with integrationComponentSubtype
  • Service Principals, OAuth Apps, machine accounts: entity_type: "identity" with identitySubtype

Why this matters:

  1. Semantic correctness: The type system should reflect reality. A Business Rule is not an identity.
  2. Query simplicity: entityType=identity should return only identities, not automations.
  3. Path materialization clarity: Identity paths traverse HAS_ROLE. Automation paths traverse RUNS_AS → HAS_ROLE.
  4. Documentation clarity: No need to explain "identity doesn't mean identity."
  5. Future extensibility: Adding new automation types (Lambda, Cloud Functions) is straightforward.

Implementation approach:

  • 8-phase rollout over 8 weeks
  • Zero downtime, backward compatible during transition
  • Total effort: 168 hours (or 136 hours without data migration)
  • 47 files impacted (21 platform TypeScript, 3 connector Python, 23 tests/docs)

Risk level: Medium. Path materialization logic changes require careful testing, but the phased approach mitigates risk.

Rollback plan: Fully reversible at every phase (Phases 0-5 have zero data changes; Phase 6 has reverse migration script).

Recommendation: Proceed with refactor. This is a foundational correctness fix that improves code clarity, query simplicity, and documentation accuracy. The cost (168 hours) is justified by long-term maintainability gains.


Analysis complete. 850 lines. 47 files audited. 3 new entity types proposed. 8-phase implementation plan. Zero breaking changes during transition. Rollback plan for every phase. Ready for founder decision.