Skip to content

Path finding

Fallen-8 finds shortest paths between two vertices with POST /path/{from}/to/{to}. Two algorithms ship in the box (an unweighted hop-count search (BLS, the default) and a weighted Dijkstra (DIJKSTRA)) and both accept optional C# filter and cost fragments that decide which vertices and edges are traversable and how much each step weighs. There is no query language: those fragments are runtime-compiled delegates (delegates.md). A path query is a read, so no transaction is involved; every route also answers under /ns/{ns}/… for a specific graph (namespaces.md).

POST /path/{from}/to/{to}: from and to are vertex ids. The body is required; send {} to use every default (a POST with no body at all is a 400).

Field Type Default Meaning
pathAlgorithmName string BLS BLS, DIJKSTRA (below), or the name of a registered Path algorithm plugin (plugin-registration.md). Matched case-sensitively; an unknown or mis-cased name yields an empty result, not an error.
maxDepth uint16 7 Maximum number of edges on a returned path. 0 returns [] immediately.
maxResults uint16 65535 Maximum paths to return. For DIJKSTRA this is the K of K-shortest, set it to 1 for just the cheapest.
maxPathWeight double unbounded Inclusive cumulative-weight cap; honoured by DIJKSTRA, ignored by BLS.
filter object (none) vertexFilter / edgeFilter / edgePropertyFilter fragments (below).
cost object (none) vertexCost / edgeCost fragments (below).
storedQuery string (none) Invoke a registered Path query by name instead of inline fragments, mutually exclusive with filter/cost (stored-queries.md).
semantic object (none) Code-free similarity filter/cost block (semantic-traversal.md).
timeBudgetSeconds double unbounded Cooperative deadline for the traversal; 408 when it is exhausted, and the request is always bound to the client’s abort as well. Checked between filter and cost invocations, so it bounds an expensive traversal but cannot interrupt a single fragment that never returns (security).

Both traverse edges in both directions (undirected reachability over directed edges); each step records the direction it actually used. Both are discovered as plugins (plugins.md); GET /statusavailablePathPlugins reports the invocable set, which is the two built-ins plus any Path algorithm plugin registered in this namespace.

BLS (default) DIJKSTRA
Optimises fewest hops least total weight
Cost block ignored: every weight is 0 consumed
maxPathWeight ignored enforced (inclusive)
Multiple results all fewest-hop paths (≤ maxResults) K least-weight loop-free paths, non-decreasing weight (Yen’s algorithm)

Pick BLS for reachability and degrees-of-separation questions where every edge counts the same. Pick DIJKSTRA when edges or vertices carry a cost to minimise, e.g. the air-routes sample’s km for the shortest flight distance, or the attack-surface sample’s exploitCost for the least-effort attack path (samples.md).

Each slot is a one-statement C# fragment of the form return (<param>) => <expr>;. Filters return bool (return false to make the element non-traversable); costs return double (the step weight). The parameter name is yours; only its type is fixed by the slot.

Slot Receives Returns Example
filter.vertexFilter VertexModel bool return (v) => v.Label == "person";
filter.edgeFilter EdgeModel bool return (e) => e.Label == "trusts";
filter.edgePropertyFilter string (the edge’s type, edgePropertyId) bool return (p) => p == "knows";
cost.vertexCost VertexModel double return (v) => 0.0;
cost.edgeCost EdgeModel double return (e) => e.TryGetProperty<double>(out var w, "weight") ? w : 1.0;

These fragments are compiled at runtime with Roslyn and run in-process; the full contract, the accessor surface, and the POST /delegates/validate compile-check live in delegates.md. Dynamic code execution is always on, so a request carrying inline fragments just needs the credential when a key is configured (security.md). Two code-free alternatives exist for callers that never compile C#: a storedQuery reference (stored-queries.md) and the semantic block (semantic-traversal.md).

Reaching a neighbour v across edge e costs edgeCost(e) + vertexCost(v), and totalWeight is the sum over the whole path. The defaults matter:

  • No cost block at all → each edge costs 1 and each vertex 0, so DIJKSTRA degenerates to a fewest-hop search whose totalWeight equals the hop count.
  • A cost block with only edgeCostvertexCost falls back to its own default return (v) => 1.0;, which adds 1 per step. For a pure edge-weight sum, set vertexCost to return (v) => 0.0; explicitly.

Costs must be non-negative; a negative step cost is clamped to 0 (logged once per query). maxDepth is enforced during the search, so a cheaper-but-longer route is correctly rejected in favour of a costlier one that fits the hop budget.

200 with a JSON array of paths, empty ([]) when none exist. [] also covers a missing (or removed) from/to vertex, from == to (a zero-length self-path is never returned), maxResults: 0, and an unknown algorithm name. A runtime fault inside the traversal or inside a compiled fragment is a 500 and is deliberately never flattened into an empty result, so [] always means “no path”, never “something broke”.

Path field Meaning
pathElements[] Ordered hops from from to to
totalWeight Sum of the element weights (0 for BLS)
Element field Type Meaning
sourceVertexId / targetVertexId int The hop’s from / to vertex
edgeId int The edge traversed on this hop
edgePropertyId string The edge’s type: the adjacency group it sits under (edge type vs label)
direction int 0 = traversed against the edge’s stored direction, 1 = with it (2 = undirected; path traversal emits only 0/1)
weight double This step’s cost (0 for BLS)

Create the built-in sample graph in an empty namespace (in it Trent = 4, Mallory = 3; the call appends rather than resets, so the ids shift if the graph already holds elements), then find every fewest-hop path between them:

curl -X PUT http://localhost:8080/unittest
curl -X POST http://localhost:8080/path/4/to/3 \
-H "Content-Type: application/json" -d '{}'

Two two-hop paths come back (Trent → Alice → Mallory and Trent → Bob → Mallory) each with weight 0, because BLS never consumes the cost block:

[
{ "pathElements": [
{ "sourceVertexId": 4, "targetVertexId": 0, "edgeId": 6, "edgePropertyId": "trusts", "direction": 0, "weight": 0 },
{ "sourceVertexId": 0, "targetVertexId": 3, "edgeId": 9, "edgePropertyId": "attacks", "direction": 0, "weight": 0 } ],
"totalWeight": 0 },
{ "pathElements": [
{ "sourceVertexId": 4, "targetVertexId": 1, "edgeId": 7, "edgePropertyId": "trusts", "direction": 0, "weight": 0 },
{ "sourceVertexId": 1, "targetVertexId": 3, "edgeId": 10, "edgePropertyId": "attacks", "direction": 0, "weight": 0 } ],
"totalWeight": 0 }
]

Example: cheapest weighted path (DIJKSTRA)

Section titled “Example: cheapest weighted path (DIJKSTRA)”

Given three vertices where A → B weighs 10 while A → C and C → B each weigh 1, the cheapest A → B route is the two-hop A → C → B (total 2), not the direct weight-10 edge. That graph needs building first. Store weight as System.Double, because TryGetProperty<double> treats a value of any other CLR type as absent and the fragment below would then fall back to 1.0 for every edge (delegates.md). The two waited-on batch calls return the assigned ids; continuing in the graph from the example above they come out as [11, 12, 13] for A, B, C and [14, 15, 16] for the three roads:

curl -X PUT "http://localhost:8080/vertices?waitForCompletion=true" \
-H "Content-Type: application/json" \
-d '[{"creationDate":0,"label":"town"},{"creationDate":0,"label":"town"},{"creationDate":0,"label":"town"}]'
curl -X PUT "http://localhost:8080/edges?waitForCompletion=true" \
-H "Content-Type: application/json" \
-d '[{"sourceVertex":11,"targetVertex":12,"edgePropertyId":"road","creationDate":0,
"properties":[{"propertyId":"weight","propertyValue":"10","fullQualifiedTypeName":"System.Double"}]},
{"sourceVertex":11,"targetVertex":13,"edgePropertyId":"road","creationDate":0,
"properties":[{"propertyId":"weight","propertyValue":"1","fullQualifiedTypeName":"System.Double"}]},
{"sourceVertex":13,"targetVertex":12,"edgePropertyId":"road","creationDate":0,
"properties":[{"propertyId":"weight","propertyValue":"1","fullQualifiedTypeName":"System.Double"}]}]'

Now read the weight off each edge with edgeCost, zero out vertexCost, and ask for the single best path:

curl -X POST http://localhost:8080/path/11/to/12 \
-H "Content-Type: application/json" \
-d '{
"pathAlgorithmName": "DIJKSTRA",
"maxResults": 1,
"cost": {
"vertexCost": "return (v) => 0.0;",
"edgeCost": "return (e) => e.TryGetProperty<double>(out var w, \"weight\") ? w : 1.0;"
}
}'
[
{ "pathElements": [
{ "sourceVertexId": 11, "targetVertexId": 13, "edgeId": 15, "edgePropertyId": "road", "direction": 1, "weight": 1 },
{ "sourceVertexId": 13, "targetVertexId": 12, "edgeId": 16, "edgePropertyId": "road", "direction": 1, "weight": 1 } ],
"totalWeight": 2 }
]

Leaving maxResults at its default would instead return every loop-free A → B route in non-decreasing weight order (here the weight-2 detour, then the weight-10 direct edge).

Code When
200 Paths found, or none ([]); a missing/removed from/to vertex, from == to, maxResults: 0, or an unknown algorithm also returns []
400 Missing or malformed body, a fragment that fails to compile (Roslyn diagnostics in the body), storedQuery mixed with inline fragments, or a storedQuery that exists but is not of kind Path
401 No credential supplied while a key is configured (security.md)
404 The referenced storedQuery name does not exist
409 The referenced storedQuery is not invocable (its recompile-on-load failed)
413 / 429 Body over 1 MiB / sensitive-endpoint rate limit exceeded
500 The traversal or a compiled filter/cost fragment threw at runtime; a genuine no-path stays 200 with []

A request that carries semantic.queryText can additionally answer 403 or 503 when the embedding provider is off or unavailable (semantic-traversal.md).

  • Delegates: the no-query-language philosophy, the fragment contract, compilation, and /delegates/validate
  • Stored queries: precompiled Path queries invoked by name, no dynamic code required
  • Semantic traversal: the code-free semantic similarity block carried on /path
  • Security: the API key that gates access to these always-on code endpoints
  • Graph model: vertices, edges, properties, and the transactions that build a graph
  • Samples: weighted datasets to traverse (air-routes km, attack-surface exploitCost)
  • Plugins: how path algorithms are discovered
  • Plugin registration: adding your own Path algorithm, invocable by name through pathAlgorithmName
  • Namespaces: per-namespace routing (/ns/{ns}/…)
  • Source: Algorithms/Path/, Delegates.cs