Skip to main content
Applies to BloodHound Enterprise and CE

Schema

In this section, you will find all the information to create a JSON file that BloodHound can ingest and use to display your Nodes and Edges. The most up-to-date JSON Schema can always be found in our CE repository. Currently, the location of the node and edge schema files in our source code can be found here.

Ingesting Generic Formatted Data

File Requirements

Acceptable formats: .json, .zip You can mix file types in a single upload (e.g. Sharphound + Generic). Compressed ZIPs containing multiple file types are supported.

JSON Format

The standard BloodHound UI upload screen now accepts files in a generic format. You can continue using it as before. At minimum, your JSON file should have these elements:
{
  "graph": {
    "nodes": [],
    "edges": []
  }
}
The nodes and edges must conform to our JSON Schema, see details below. The validation of the data occurs at upload time. When ingest completes, the generic data will be available via Cypher search ONLY. Generic data is not searchable via the pathfinding feature (yet). Entity Panels: clicking on a generic node or edge will only render the entity’s property bag. At this time there is no support for defining entity panels for generic entities.

Nodes

Property Rules

  • Properties must be primitive types or arrays of primitive types.
  • Nested objects and arrays of objects are not allowed.
  • Arrays must be homogeneous (e.g. all strings or all numbers).
  • An array of kind labels for the node. The first element is treated as the node’s primary kind and is used to determine which icon to display in the graph UI. This primary kind is only used for visual representation and has no semantic significance for data processing.
  • Property names must be lowercase. Property names in BloodHound are case-sensitive, which means objectid and ObjectID are treated as two distinct properties on the same node. This can cause issues when parsing API responses in languages that treat JSON keys as case-insensitive (for example, PowerShell and some .NET-based tools).
    A future release will enforce lowercase property names. To avoid breaking changes, adopt lowercase property names now.

Node JSON

The following is the JSON schema that all nodes must conform to.
{
    "title": "Generic Ingest Node",
    "description": "A node used in a generic graph ingestion system. Each node must have a unique identifier (`id`) and at least one kind describing its role or type. Nodes may also include a `properties` object containing custom attributes.",
    "type": "object",
    "properties": {
        "id": { "type": "string" },
        "properties": {
            "type": ["object", "null"],
            "description": "A key-value map of node 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": {
                "type": ["string", "number", "boolean", "array"],
                "items": {
                    "not": {
                        "type": "object"
                    }
                }
            }
        },
        "kinds": {
            "type": ["array"],
            "items": { "type": "string" },
            "maxItems": 3,
            "minItems": 1,
            "description": "An array of kind labels for the node. The first element is treated as the node's primary kind and is used to determine which icon to display in the graph UI. This primary kind is only used for visual representation and has no semantic significance for data processing."
        }
    },
    "required": ["id", "kinds"],
    "examples": [
        {
            "id": "user-1234",
            "kinds": ["Person"]
        },
        {
            "id": "device-5678",
            "properties": {
                "manufacturer": "Brandon Corp",
                "model": "4000x",
                "isactive": true,
                "rating": 43.50
            },
            "kinds": ["Device", "Asset"]
        },
        {
            "id": "location-001",
            "properties": null,
            "kinds": ["Location"]
        }
    ]
}

Edges

Edge kind values must contain only letters (A-Z, a-z), numbers (0-9), and underscores (_). BloodHound validates edge kinds at upload time. If any edge kind includes spaces, dashes (-), backticks, or other special characters, the file upload fails. We recommend using PascalCase (for example, AdminTo, HasSession) 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. See Neo4j Escaping rules for symbolic names for additional naming guidance.

Edge kind validation

BloodHound enforces this pattern for edge kinds:
^[A-Za-z0-9_]+$
Valid examples
  • AdminTo
  • HasSession
  • AZMGGrantRole
  • Edge_1
Invalid examples
  • Admin-To
  • Has Session
  • Has`Session
  • Has$Session

Edge JSON

The following is the JSON schema that all edges must conform to. If an edge kind does not meet the allowed pattern, BloodHound returns a schema validation error and rejects the upload.
{
    "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 either a unique identifier (id) or a name-based lookup. 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",
    "properties": {
        "start": {
            "type": "object",
            "properties": {
                "match_by": {
                    "type": "string",
                    "enum": ["id", "name"],
                    "default": "id",
                    "description": "Whether to match the start node by its unique object ID or by its name property."
                },
                "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."
                }
            },
            "required": ["value"]
        },
        "end": {
            "type": "object",
            "properties": {
                "match_by": {
                    "type": "string",
                    "enum": ["id", "name"],
                    "default": "id",
                    "description": "Whether to match the end node by its unique object ID or by its name property."
                },
                "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."
                }
            },
            "required": ["value"]
        },
        "kind": {
            "type": "string",
            "description": "The relationship type. Must contain only letters, numbers, and underscores.",
            "pattern": "^[A-Za-z0-9_]+$"
        },
        "properties": {
            "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": {
                "type": ["string", "number", "boolean", "array"],
                "items": {
                    "not": {
                        "type": "object"
                    }
                }
            }
        }
    },
    "required": ["start", "end", "kind"],
    "examples": [
        {
            "start": {
                "match_by": "id",
                "value": "user-1234"
            },
            "end": {
                "match_by": "id",
                "value": "server-5678"
            },
            "kind": "HasSession",
            "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": "AccessedResource",
            "properties": {
                "via": "SMB",
                "sensitive": true
            }
        },
        {
            "start": {
                "value": "admin-1"
            },
            "end": {
                "value": "domain-controller-9"
            },
            "kind": "AdminTo",
            "properties": {
                "reason": "elevated_permissions",
                "confirmed": false
            }
        },
        {
            "start": {
                "match_by": "name",
                "value": "Printer-007"
            },
            "end": {
                "match_by": "id",
                "value": "network-42"
            },
            "kind": "ConnectedTo",
            "properties": null
        }
    ]
}

Post-processing

Post-processing in BloodHound refers to the analysis phase where the system creates certain edges after ingesting data to identify attack paths. After ingesting data, BloodHound analyzes the graph state and adds edges it considers useful. BloodHound regenerates “post-processed” edges after it builds a complete graph. Before regenerating post-processed edges, BloodHound deletes any existing ones. As a result, BloodHound removes any post-processed edges that you add directly to an OpenGraph payload.
BloodHound creates the following edges during post-processing:
You can work around this behavior by including the supporting edges that cause the post-processing step to generate the edge that you want. 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"
            }
        ]
    }
}

Optional Metadata Field

You can optionally include a metadata object at the top level of your JSON payload. This metadata currently supports a single field:
  • source_kind: a string that applies to all nodes in the file, used to attribute a source to ingested nodes (e.g. Github, Snowflake, MSSQL). This is useful for tracking where a node originated. We internally use this concept already for AD/Azure, using the labels “Base” and “AZBase” respectively.
Example:
{
  "metadata": {
    "source_kind": "GHBase"
  },
  "graph": {
    "nodes": [],
    "edges": []
  }
}
If present, the source_kind will be added to the kinds list of all nodes in the file during ingest. This feature is optional.

Minimal Working JSON

The following is a minimal example payload that conforms to the node and edge schemas above. You can use this as a starting point to build your own OpenGraph. Copy and paste the following example into a new .json file or download this example file.
When working with JSON files, use a plain text editor and UTF-8 encoding. Some text editors may introduce unexpected, non-standard characters that can cause parsing errors. It’s always a good idea to validate your JSON with a linter before uploading it to BloodHound.
{
  "graph": {
    "nodes": [
      {
        "id": "123",
        "kinds": [
          "Person"
        ],
        "properties": {
          "displayname": "bob",
          "property": "a",
          "objectid": "123",
          "name": "BOB"
        }
      },
      {
        "id": "234",
        "kinds": [
          "Person"
        ],
        "properties": {
          "displayname": "alice",
          "property": "b",
          "objectid": "234",
          "name": "ALICE"
        }
      }
    ],
    "edges": [
      {
        "kind": "Knows",
        "start": {
          "value": "123",
          "match_by": "id"
        },
        "end": {
          "value": "234",
          "match_by": "id"
        }
      }
    ]
  }
}
To test the ingestion in your BloodHound instance, navigate to ExploreCypher. Enter the following query and hit Run:
match p=()-[:Knows]-()
return p
You should get something that looks like this: BOB->Knows->Alice