Skip to content

Graph model

Fallen-8 is a directed property graph held entirely in memory. A graph is a set of vertices and edges; both are graph elements carrying an engine-assigned integer id, an optional label, timestamps, and typed key/value properties. Every change goes through a single transaction queue (one writer thread; readers never block); reads and full-graph property scans run straight against the live graph. This page covers the model, the REST CRUD surface, and POST /scan/graph/property/.... Index-backed scans live in Indexes; C# query delegates in Delegates.

Concept Detail
Vertex A node. Has an id, optional label, and properties.
Edge A directed relationship from a source vertex to a target vertex. Edges are graph elements too: an edge has its own id, label, and properties, plus a required edgePropertyId, which is its type.
edgePropertyId The edge’s type (e.g. knows, trusts). Required, set at creation. See Edge type vs label.
label A free-form category string on any element (e.g. person, friendship). Optional.
Properties A per-element map of string key -> typed value. Empty by default; keys are unique per element.

Every element also carries system fields: id (assigned by the engine on commit: you do not choose it), creationDate, and modificationDate. Over REST, create requests take creationDate as a Unix-seconds uint (0 is fine); reads return both timestamps as ISO-8601. Vertices and edges share one id space: ids are handed out from a single counter in creation order, so an id names exactly one element of either kind (hence the kind-agnostic /graphelement/{id} routes, and GET /vertex/{id} on an edge id answering 204), and edge ids interleave with vertex ids. Property keys beginning with $embedding: / $embeddingModel: are reserved for engine-managed vector state; see Semantic traversal. They are not filtered out of reads: an embedded element’s properties array lists $embedding:<name> with fullQualifiedTypeName System.Single[] and a bracketed component list as its value.

An edge carries two classifier strings that are easy to conflate. They do different jobs:

  • edgePropertyId is the edge’s type: structural. It names the adjacency group the edge occupies on both endpoints: a vertex’s outgoing/incoming edges are stored per type, and the whole traversal surface keys on it, GET /vertex/{id}/edges/out/{edgePropertyId}, degree routes, the path and subgraph edgePropertyFilter, and analytics scoping. It is required at creation and, despite the name, it is not one of the edge’s key/value properties.
  • label is an optional category tag: decorative. It is the same field vertices have, and serves scans (label restrictor), bulk-export filters, and statistics. On an edge it is an orthogonal second grouping, typically a human-facing one, e.g. type suppliesTrojan, label supplies trojan.

Most graphs set only the type and leave edge labels unset. Don’t copy the type into the label: add a label only when it says something the type doesn’t. Reads return both: every edge read (GET /edge/{id}, GET /graph, path results, change-feed edgeCreated events) carries edgePropertyId alongside label.

A property value crosses REST as a JSON string plus a fullQualifiedTypeName that names the target .NET type. The string is parsed to that type with InvariantCulture (so "0.8" is always 0.8, never 8). Only this closed allow-list of primitive types is accepted:

String, Boolean, Byte, SByte, Int16, UInt16, Int32, UInt32, Int64, UInt64, Single, Double, Decimal, Char, DateTime, DateTimeOffset, TimeSpan, Guid.

The name is case-insensitive and accepts the full name (System.Int32), the short name (Int32), or the C# alias (int). An unknown name is a 400 on the scan routes and on PUT /graphelement/{id}/{propertyId}, which also answer 400 for a value that cannot be parsed to the named type. On the create routes (PUT /vertex, /vertices, /edge, /edges) neither case is caught: both currently surface as a 500.

Always send all three fields of a property (propertyId, propertyValue, fullQualifiedTypeName). They are schema-required but each carries a placeholder default (cacheSize, 1000, System.Int32), so an omitted field is not rejected: it silently takes the placeholder.

Mutation goes through the transaction queue

Section titled “Mutation goes through the transaction queue”

All writes (create, property add/remove, delete) are enqueued and applied by one writer thread: serialized, while lock-free readers keep running. Over REST this is the waitForCompletion query parameter, carried by the element write endpoints (PUT /vertex, /vertices, /edge, /edges and the PUT/DELETE /graphelement/... routes). The maintenance routes below have no such parameter: HEAD /tabularasa and HEAD /trim only enqueue, and answer 200 with an empty body.

waitForCompletion Behaviour
false (default) The write is enqueued and the call returns 202 Accepted immediately.
true The call awaits the outcome. A committed write is 202 (200 with the assigned ids for the batch creates); a rolled-back write maps to 400/404/409/500 by cause.

The single-element creates return no body: the new id is not echoed back. When you need the assigned ids, use the batch creates PUT /vertices / PUT /edges with waitForCompletion=true; otherwise locate a just-created element with a property scan (below) or GET /graph. Writer/commit internals are in Architecture.

Base URL http://localhost:8080. These are the bare (default-namespace) paths; every route also answers under /ns/{name}/...; see Namespaces.

Method & path Purpose
PUT /vertex Create a vertex. Body: VertexSpecification.
PUT /vertices Create many vertices in ONE atomic transaction. Body: an array of VertexSpecification. Waited: 200 with the assigned ids in input order.
PUT /edge Create an edge (404 if a referenced endpoint is missing and waitForCompletion=true). Body: EdgeSpecification.
PUT /edges Create many edges in ONE atomic transaction. Body: an array of EdgeSpecification. Waited: 200 with the assigned ids; a single missing endpoint rolls the whole batch back and answers 404.
GET /vertex/{id} · GET /edge/{id} Read one element (200, or 204 when absent).
GET /graphelement/{id} Read a vertex or edge by id.
POST /graphelements/get Read MANY elements by id in one call. Body: an array of ids. Duplicates collapse, ids that do not exist come back in a separate notFound list rather than as an error, and the page is capped. One request instead of one per element, which is what a client holding a few hundred resolved ids needs.
GET /graph?maxElements=1000 A bounded page of vertices and edges (clamped to 100000 each).
PUT /graphelement/{id}/{propertyId} Add or update one property. Body: PropertySpecification.
DELETE /graphelement/{id}/{propertyId} Remove one property.
PUT /graphelements/properties Set and remove properties across MANY elements in ONE atomic transaction, with replace semantics. Either every change applies or none does; a missing element rolls the whole batch back. This is the route to use when a client is reconciling a set of elements against a source, instead of one call per property.
DELETE /graphelement/{id} Remove a vertex or edge. Removing a vertex cascades to every incident edge.
DELETE /graphelements Remove many elements in one atomic transaction. Body: an array of ids. Each vertex removal cascades to its incident edges as above.
HEAD /tabularasa Erase all data in the addressed namespace (it stays registered, empty). Every index definition goes with it, not just its entries (Indexes).
HEAD /tabularasa/all Fallen-8-level factory reset: drops every non-default namespace and erases default. Irreversible, and rate-limited (429).
HEAD /trim Enqueue a memory trim (releases unused memory). Changes no data.
GET /vertex/count · GET /edge/count Element counts.

Two of those rows have consequences worth spelling out:

  • Removing a vertex removes its edges. The cascade deletes every incident edge, so GET /edge/count drops with it, and the change feed reports one edgeRemoved per cascaded edge next to the vertexRemoved. There is no detach-only option.
  • GET /graph bounds vertices and edges independently. maxElements is applied to each of the two reads separately (and a negative value yields an empty page), so a truncated page can contain edges whose sourceVertex/targetVertex are not in its own vertices array. Resolve those with GET /vertex/{id}, raise the bound, or take an internally consistent dump with GET /bulk/export (Bulk import/export).

A read returns the system fields, the properties as an array, and, for a vertex, its full adjacency as edge type -> edge ids. That adjacency often saves the per-group traversal calls below (creationDate: 0 reads back as the 1970 epoch):

{
"id": 7,
"creationDate": "1970-01-01T00:00:00",
"modificationDate": "1970-01-01T00:00:00",
"label": "person",
"properties": [
{ "propertyId": "name", "fullQualifiedTypeName": "System.String", "propertyValue": "Trent" },
{ "propertyId": "age", "fullQualifiedTypeName": "System.Int32", "propertyValue": "35" }
],
"outEdges": { "trusts": [12] },
"inEdges": { "trusts": [9, 11] }
}

An edge read carries sourceVertex, targetVertex and edgePropertyId instead of the two adjacency maps.

Traversal reads around an element. Absence is reported differently per row: the group and edge-id list routes answer 204 when the vertex or the group is missing, the degree routes and GET /edge/{id}/source|target answer 404 when the vertex or edge does not exist, and a per-group degree on a live vertex that simply has no such group is 200 with 0.

Method & path Returns
GET /vertex/{id}/edges/out · .../edges/in The edgePropertyId groups present.
GET /vertex/{id}/edges/out/{edgePropertyId} · .../in/{edgePropertyId} Edge ids in that group.
GET /vertex/{id}/edges/outdegree · .../indegree Total out/in degree.
GET /vertex/{id}/edges/out/{edgePropertyId}/degree · .../in/.../degree Degree within a group.
GET /edge/{id}/source · GET /edge/{id}/target The endpoint vertex ids.

Creating a vertex, in both shells:

curl -X PUT http://localhost:8080/vertex \
-H 'Content-Type: application/json' \
-d '{
"creationDate": 0,
"label": "person",
"properties": [
{ "propertyId": "name", "propertyValue": "Trent", "fullQualifiedTypeName": "System.String" },
{ "propertyId": "age", "propertyValue": "35", "fullQualifiedTypeName": "System.Int32" }
]
}'

An edge is created the same way against PUT /edge, with this body shape (label is optional, so a minimal edge sets only its type: see Edge type vs label):

{
"sourceVertex": 0,
"targetVertex": 4,
"edgePropertyId": "trusts",
"creationDate": 0
}

PUT /vertices and PUT /edges take a JSON array of the same bodies and commit it as one transaction. Because edges reference ids that already exist, the usual sequence is: post the vertices with waitForCompletion=true, read the returned ids, then post the edges. File-based loading and dumping are covered in Bulk import/export.

POST /scan/graph/property/{propertyId} walks every element and returns the ids of those whose {propertyId} value satisfies the comparison. It is a linear O(n) scan with no index, for indexed lookups use Indexes. Request body:

Field Value
operator The comparison, as an integer: 0 Equals, 1 Greater, 2 GreaterOrEquals, 3 Lower, 4 LowerOrEquals, 5 NotEquals.
literal { "value": "<string>", "fullQualifiedTypeName": "<type>" }, the value to compare against, typed as above.
label Optional exact-match label restrictor on the hits; omit to scan every label.
resultType "Vertices", "Edges", or "Both".

The response is a 200 with a JSON array of matching ids (empty when nothing matches); a missing literal or an unknown/unconvertible type is a 400.

Worked example against the built-in sample graph (create it with PUT /unittest; the graph it builds is listed under Path finding), which stores each person’s name under name. Find “Trent”:

curl -X POST http://localhost:8080/scan/graph/property/name \
-H 'Content-Type: application/json' \
-d '{ "operator": 0,
"literal": { "value": "Trent", "fullQualifiedTypeName": "System.String" },
"resultType": "Vertices" }'
# => [4]

When you do not yet know which key holds a value, POST /scan/graph/properties (plural) walks every element and returns the ids of those where any property value contains a search term. It is the companion to the singular scan above: singular compares one named key with a typed operator; plural does a case-insensitive substring match across all values. Each value is rendered to its invariant text form first, so numbers, booleans and dates are searchable too (searching 42 finds the integer 42). It is a linear O(n) scan with no index and no score; reserved embedding entries are never matched. Request body:

Field Value
searchTerm The substring to look for across every property value (case-insensitive). Required and non-blank.
label Optional exact-match label restrictor on the hits; omit to scan every label.
resultType "Vertices", "Edges", or "Both" (defaults to "Both").

The response is a 200 with a JSON array of matching ids (empty when nothing matches); a missing or blank searchTerm is a 400.

curl -X POST http://localhost:8080/scan/graph/properties \
-H 'Content-Type: application/json' \
-d '{ "searchTerm": "Trent", "resultType": "Vertices" }'
# => [4]

Embedded in-process, the same model is reached without REST. Build a transaction, enqueue it, and wait; read with the Try*(out result, ...) : bool pattern.

var f8 = new Fallen8(loggerFactory);
// Write: enqueue, then wait for the writer thread to commit.
var tx = new CreateVerticesTransaction();
tx.AddVertex(0, "person", new Dictionary<string, object> { { "name", "Trent" } });
f8.EnqueueTransaction(tx).WaitUntilFinished();
// Read: no queue, never blocks.
if (f8.TryGetVertex(out var v, 4) && v.TryGetProperty<string>(out var name, "name"))
{
// name == "Trent"; v.GetOutDegree(), v.OutEdges, ...
}
// Full-graph scan.
f8.GraphScan(out var hits, "name", "Trent", BinaryOperator.Equals);

Reads (TryGetVertex, TryGetEdge, TryGetGraphElement, GetAllVertices, GetAllEdges, GetAllGraphElements, GraphScan, GraphScanAllProperties) live on IFallen8Read; EnqueueTransaction on IFallen8Write. It returns a TransactionInformation whose Completion task and WaitUntilFinished() await the outcome and whose TransactionState / FailureReason report a rollback.