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.
Vertices, edges, and properties
Section titled “Vertices, edges, and properties”| 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.
Edge type vs label
Section titled “Edge type vs label”An edge carries two classifier strings that are easy to conflate. They do different jobs:
edgePropertyIdis 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 subgraphedgePropertyFilter, and analytics scoping. It is required at creation and, despite the name, it is not one of the edge’s key/value properties.labelis an optional category tag: decorative. It is the same field vertices have, and serves scans (labelrestrictor), bulk-export filters, and statistics. On an edge it is an orthogonal second grouping, typically a human-facing one, e.g. typesuppliesTrojan, labelsupplies 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.
Typed property values
Section titled “Typed property values”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.
REST CRUD
Section titled “REST CRUD”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/countdrops with it, and the change feed reports oneedgeRemovedper cascaded edge next to thevertexRemoved. There is no detach-only option. GET /graphbounds vertices and edges independently.maxElementsis applied to each of the two reads separately (and a negative value yields an empty page), so a truncated page can contain edges whosesourceVertex/targetVertexare not in its ownverticesarray. Resolve those withGET /vertex/{id}, raise the bound, or take an internally consistent dump withGET /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" } ] }'$body = @'{ "creationDate": 0, "label": "person", "properties": [ { "propertyId": "name", "propertyValue": "Trent", "fullQualifiedTypeName": "System.String" }, { "propertyId": "age", "propertyValue": "35", "fullQualifiedTypeName": "System.Int32" } ]}'@Invoke-RestMethod http://localhost:8080/vertex -Method Put -ContentType 'application/json' -Body $bodyAn 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.
Full-graph property scans
Section titled “Full-graph property scans”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]$body = @'{ "operator": 0, "literal": { "value": "Trent", "fullQualifiedTypeName": "System.String" }, "resultType": "Vertices" }'@Invoke-RestMethod http://localhost:8080/scan/graph/property/name -Method Post -ContentType 'application/json' -Body $body# => 4Across every property (discovery search)
Section titled “Across every property (discovery search)”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]$body = @'{ "searchTerm": "Trent", "resultType": "Vertices" }'@Invoke-RestMethod http://localhost:8080/scan/graph/properties -Method Post -ContentType 'application/json' -Body $body# => 4Using the engine as a library
Section titled “Using the engine as a library”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.
See also
Section titled “See also”- Indexes: index types and
POST /scan/index/.... - Delegates: why there is no query language; C# predicates over the graph.
- Path finding: traversals between vertices.
- Namespaces: isolated graphs and
/ns/{name}routing. - Semantic traversal: element embeddings and reserved property keys.
- Bulk import/export: creating and dumping graphs in bulk.
- REST API: versioning, OpenAPI, and shared conventions.