Skip to main content

Documentation Index

Fetch the complete documentation index at: https://bloodhound.specterops.io/llms.txt

Use this file to discover all available pages before exploring further.

Applies to BloodHound Enterprise and CE OpenGraph edges define relationships between nodes. Use this page to validate edge kinds, endpoint matching behavior, and post-processing outcomes before ingest.
Use this page to validate structure before ingestion. For a full data payload example, see Graph Data.
At minimum, each edge must include a start endpoint, an end endpoint, and a kind that describes the relationship type.
{
  "graph": {
    "nodes": [],
    "edges": [
      {
        "start": {
          "match_by": "id",
          "value": "node-12345"
        },
        "end": {
          "match_by": "id",
          "value": "node-67890"
        },
        "kind": "RelationshipType"
      }
    ]
  }
}
start
object
required
An object that defines how to match the starting node of the edge. See Endpoint Matching.
end
object
required
An object that defines how to match the ending node of the edge. See Endpoint Matching.
kind
string
required
A string that describes the relationship type.
  • A descriptive name that identifies the edge kind and does not overlap with built-in edge kinds. Consider using a prefix related to your data source or environment separated from the name by an underscore. For example, Okta_ResetPassword. For structured graphs, a prefix that matches the extension’s namespace is required.
  • Must match the regex pattern ^[A-Za-z0-9_]+$, which means edge kinds can only contain uppercase letters, lowercase letters, numbers, and underscores. Spaces, dashes, backticks, and other special characters are not allowed in edge kinds. PascalCase is recommended for readability and consistency. Neo4j Cypher allows many special characters in symbolic names when the name is enclosed in backticks. BloodHound OpenGraph ingest is more restrictive: edge kind values must match ^[A-Za-z0-9_]+$, so upload validation rejects backtick-escaped names, spaces, dashes, and other special characters.
properties
object
A key-value map of custom edge properties. Values must be strings, numbers, booleans, or arrays of primitives. Nested objects and arrays of objects are not allowed.
Unless otherwise noted, examples below show edge objects only.

Endpoint Matching

Edges in OpenGraph data define relationships between nodes using a start endpoint object and an end endpoint object. You can control how BloodHound resolves each endpoint using one of three matching strategies in the match_by field.
  • id for direct node identifier matching
  • property for node property matching
  • name for legacy node name matching (deprecated)
This flexibility allows you to link nodes based on their unique database identifiers or by dynamically finding them based on specific property values.
Use identifier matching when possible. Property matching is more flexible, but it is slower and should be used only when you cannot match by node ID.

Match by Identifier

This is the default and most common method. It resolves an endpoint by unique node id.
Linking a specific user to a server using their unique IDs
{
  "start": {
    "match_by": "id",
    "value": "user-12345"
  },
  "end": {
    "match_by": "id",
    "value": "server-98765"
  }
}
start
object
required
Starting endpoint definition for the edge.
start.match_by
string
Strategy for matching the starting node. If omitted, defaults to id for unique identifier matching.
start.value
string
required
String containing the specific ID of the starting node.
end
object
required
Ending endpoint definition for the edge.
end.match_by
string
Strategy for matching the ending node. If omitted, defaults to id for unique identifier matching.
end.value
string
required
String containing the specific ID of the ending node.
start.kind
string
Optional kind filter used in start to constrain the lookup to a specific node kind.For example, setting kind: "User" ensures that even if a name exists across multiple entity types, only the one classified as a User is selected.
end.kind
string
Optional kind filter used in end to constrain the lookup to a specific node kind.For example, setting kind: "Server" limits endpoint resolution to nodes classified as Server.
start.property_matchers
array
Not used in this mode. If provided alongside start.match_by: "id", validation fails.
end.property_matchers
array
Not used in this mode. If provided alongside end.match_by: "id", validation fails.

Match by Property

Use this strategy when you do not know the unique ID of the target node but can identify it using one or more known properties (for example, username, email address, hostname, or custom property). This method allows for dynamic resolution based on data available at the time of ingestion. To use this strategy, set the match_by property to property.
Linking a user to a server by matching the user's username property and the server's hostname property
{
  "start": {
    "match_by": "property",
    "property_matchers": [
      {
        "key": "username",
        "operator": "equals",
        "value": "alice.smith"
      },
      {
        "key": "active",
        "operator": "equals",
        "value": true
      }
    ],
    "kind": "User"
  },
  "end": {
    "match_by": "property",
    "property_matchers": [
      {
        "key": "hostname",
        "operator": "equals",
        "value": "db-prod-01"
      }
    ]
  }
}
start
object
required
Starting endpoint definition for the edge.
start.match_by
string
required
Strategy for matching the starting node. Set to property to match against one or more of the starting node’s property values.
start.property_matchers
array
required
Array of matchers used to find the starting node. BloodHound attempts to find a node that satisfies all matchers.At least one matcher is required, but you can provide multiple matchers in the array. The system will attempt to find a node that satisfies all conditions simultaneously.
start.kind
string
Optional kind filter used to narrow node resolution for the starting node.
end
object
required
Ending endpoint definition for the edge.
end.match_by
string
required
Strategy for matching the ending node. Set to property to match against one or more of the ending node’s property values.
end.property_matchers
array
required
Array of matchers used to find the ending node.At least one matcher is required, but you can provide multiple matchers in the array. The system will attempt to find a node that satisfies all conditions simultaneously.
start.value
string | number | boolean
Not used in this mode. Providing start.value when start.match_by is property causes validation errors.
end.value
string | number | boolean
Not used in this mode. Providing end.value when end.match_by is property causes validation errors.
property_matchers[].key
string
required
Name of the node property to check.
property_matchers[].operator
string
required
Matching operator. equals is currently the only supported value.
property_matchers[].value
string | number | boolean
required
Expected value for the property matcher.

Match by Name (deprecated)

Use this legacy strategy to resolve an endpoint by a name string. This strategy predates Match by Property and uses the value field directly, similar to Match by Identifier. It is retained for backward compatibility.
The name matching strategy is deprecated and will be removed in a future release. Migrate to Match by Property with an equality matcher on the name property. Mixing match_by: "name" with property_matchers fails schema validation.
To use this strategy, set the match_by property to name and provide the name string in value. Combine with an optional kind filter to disambiguate nodes that share names across kinds.
Linking a user to a server by their names
{
  "start": {
    "match_by": "name",
    "value": "alice",
    "kind": "User"
  },
  "end": {
    "match_by": "name",
    "value": "file-server-1",
    "kind": "Server"
  }
}
Internally, BloodHound rewrites a name match to a property match against the name property. The equivalent payload using Match by Property:
{
  "start": {
    "match_by": "property",
    "property_matchers": [
      {
        "key": "name",
        "operator": "equals",
        "value": "alice"
      }
    ],
    "kind": "User"
  },
  "end": {
    "match_by": "property",
    "property_matchers": [
      {
        "key": "name",
        "operator": "equals",
        "value": "file-server-1"
      }
    ],
    "kind": "Server"
  }
}
start
object
required
Starting endpoint definition for the edge.
start.match_by
string
required
Strategy for matching the starting node. Set to name to match against a name string.
start.value
string
required
Name string used to look up the starting node.
start.kind
string
Optional kind filter used to constrain the lookup to a specific node kind. Recommended when names overlap across kinds.
end
object
required
Ending endpoint definition for the edge.
end.match_by
string
required
Strategy for matching the ending node. Set to name to match against a name string.
end.value
string
required
Name string used to look up the ending node.
end.kind
string
Optional kind filter used to constrain the lookup to a specific node kind. Recommended when names overlap across kinds.
start.property_matchers
array
Not used in this mode. Providing property_matchers when start.match_by is name causes validation errors.
end.property_matchers
array
Not used in this mode. Providing property_matchers when end.match_by is name causes validation errors.

Post-processing

Post-processing in BloodHound runs during the analysis phase. During this phase, BloodHound generates specific edges to enrich the graph and reflect the evaluated graph state. After ingest completes, BloodHound builds a complete graph, deletes existing post-processed edges, and regenerates them. As a result, post-processed edge kinds that you add directly in OpenGraph payloads do not persist.
BloodHound creates the following edges during post-processing:
To create one of BloodHound’s built-in post-processed edges using OpenGraph, include the supporting edges that cause BloodHound to generate that relationship during post-processing. For example, if you include an AdminTo edge directly in your OpenGraph payload, BloodHound removes it during post-processing and the edge does not persist in the final graph as expected. Instead of adding AdminTo edges directly, include the supporting edges that cause the post-processor to generate the AdminTo edge. The common pattern that triggers the creation of the AdminTo edge is: See the following example OpenGraph payload that produces the effect:
{
  "graph": {
    "nodes": [
      {
        "id": "TESTNODE",
        "kinds": ["User"]
      }
    ],
    "edges": [
      {
        "start": {
          "match_by": "id",
          "value": "TESTNODE"
        },
        "end": {
          "match_by": "id",
          "value": "S-1-5-21-2697957641-2271029196-387917394-2171-544"
        },
        "kind": "MemberOfLocalGroup"
      }
    ]
  }
}

Schema

Use the schema below as the source of truth for validation requirements. You can also download the same schema as a file: opengraph-edge.json.
{
  "title": "Generic Ingest Edge",
  "description": "Defines an edge between two nodes in a generic graph ingestion system. Each edge specifies a start and end node using one of three matching strategies: by unique identifier (match_by: id), by name (match_by: name, deprecated), or by one or more property matchers (match_by: property). A kind is required to indicate the relationship type. Optional properties may include custom attributes. You may optionally constrain the start or end node to a specific kind using the kind field inside each reference.",
  "type": "object",
  "$defs": {
    "property_map": {
      "type": ["object", "null"],
      "description": "A key-value map of edge attributes. Values must not be objects. If a value is an array, it must contain only primitive types (e.g., strings, numbers, booleans) and must be homogeneous (all items must be of the same type).",
      "additionalProperties": {
        "anyOf": [
          { "type": "string" },
          { "type": "number" },
          { "type": "boolean" },
          {
            "type": "array",
            "anyOf": [
              { "items": { "type": "string" } },
              { "items": { "type": "number" } },
              { "items": { "type": "boolean" } }
            ]
          }
        ]
      }
    },
    "endpoint": {
      "type": "object",
      "properties": {
        "match_by": {
          "type": "string",
          "enum": ["id", "name", "property"],
          "default": "id",
          "description": "Whether to match the start node by its unique object ID or by a series of property matches. Note that the name value here is deprecated and will be removed in future versions. Users are advised to use the multi-property match strategy moving forward."
        },
        "property_matchers": {
          "type": "array",
          "minItems": 1,
          "items": {
            "type": "object",
            "properties": {
              "key": {
                "type": "string"
              },
              "operator": {
                "type": "string",
                "enum": ["equals"]
              },
              "value": {
                "type": ["string", "number", "boolean"]
              }
            },
            "required": ["key", "operator", "value"]
          }
        },
        "value": {
          "type": "string",
          "description": "The value used for matching — either an object ID or a name, depending on match_by."
        },
        "kind": {
          "type": "string",
          "description": "Optional kind filter; the referenced node must have this kind."
        }
      },
      "if": {
        "allOf": [
          {
            "properties": {
              "match_by": {
                "type": "string",
                "const": "property"
              }
            }
          },
          {
            "not": {
              "properties": {
                "match_by": {
                  "type": "null"
                }
              }
            }
          }
        ]
      },
      "then": {
        "required": ["property_matchers"],
        "not": {
          "required": ["value"]
        }
      },
      "else": {
        "required": ["value"],
        "not": {
          "required": ["property_matchers"]
        }
      }
    }
  },
  "properties": {
    "start": {
      "$ref": "#/$defs/endpoint"
    },
    "end": {
      "$ref": "#/$defs/endpoint"
    },
    "kind": {
      "type": "string",
      "description": "Edge kind name must contain only alphanumeric characters and underscores.",
      "pattern": "^[A-Za-z0-9_]+$"
    },
    "properties": {
      "$ref": "#/$defs/property_map"
    }
  },
  "required": ["start", "end", "kind"],
  "examples": [
    {
      "start": {
        "match_by": "id",
        "value": "user-1234"
      },
      "end": {
        "match_by": "id",
        "value": "server-5678"
      },
      "kind": "has_session",
      "properties": {
        "timestamp": "2025-04-16T12:00:00Z",
        "duration_minutes": 45
      }
    },
    {
      "start": {
        "match_by": "property",
        "property_matchers": [
          {
            "key": "prop_1",
            "operator": "equals",
            "value": "value"
          }
        ]
      },
      "end": {
        "match_by": "id",
        "value": "server-5678"
      },
      "kind": "has_session",
      "properties": {
        "timestamp": "2025-04-16T12:00:00Z",
        "duration_minutes": 45
      }
    },
    {
      "start": {
        "match_by": "name",
        "value": "alice",
        "kind": "User"
      },
      "end": {
        "match_by": "name",
        "value": "file-server-1",
        "kind": "Server"
      },
      "kind": "accessed_resource",
      "properties": {
        "via": "SMB",
        "sensitive": true
      }
    },
    {
      "start": {
        "value": "admin-1"
      },
      "end": {
        "value": "domain-controller-9"
      },
      "kind": "admin_to",
      "properties": {
        "reason": "elevated_permissions",
        "confirmed": false
      }
    },
    {
      "start": {
        "match_by": "name",
        "value": "Printer-007"
      },
      "end": {
        "match_by": "id",
        "value": "network-42"
      },
      "kind": "connected_to",
      "properties": null
    }
  ]
}

Troubleshooting

  • Upload fails on edge kind pattern: Ensure kind matches ^[A-Za-z0-9_]+$.
  • Endpoint validation fails: Use either value for id/name matching, or property_matchers for property matching, not both.
  • Expected edge disappears after ingest: Check whether it is a post-processed edge kind.