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).
The request
Section titled “The request”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). |
Algorithms
Section titled “Algorithms”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 /status → availablePathPlugins 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).
Filters and costs
Section titled “Filters and costs”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).
Cost model (DIJKSTRA)
Section titled “Cost model (DIJKSTRA)”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
costblock at all → each edge costs1and each vertex0, soDIJKSTRAdegenerates to a fewest-hop search whosetotalWeightequals the hop count. - A
costblock with onlyedgeCost→vertexCostfalls back to its own defaultreturn (v) => 1.0;, which adds1per step. For a pure edge-weight sum, setvertexCosttoreturn (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.
Response
Section titled “Response”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) |
Example: all shortest paths (BLS)
Section titled “Example: all shortest paths (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/unittestcurl -X POST http://localhost:8080/path/4/to/3 \ -H "Content-Type: application/json" -d '{}'Invoke-RestMethod -Method Put -Uri http://localhost:8080/unittestInvoke-RestMethod -Method Post -Uri http://localhost:8080/path/4/to/3 -ContentType application/json -Body '{}'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"}]}]'$towns = '[{"creationDate":0,"label":"town"},{"creationDate":0,"label":"town"},{"creationDate":0,"label":"town"}]'Invoke-RestMethod -Method Put -Uri "http://localhost:8080/vertices?waitForCompletion=true" -ContentType application/json -Body $towns
$roads = @'[{"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"}]}]'@Invoke-RestMethod -Method Put -Uri "http://localhost:8080/edges?waitForCompletion=true" -ContentType application/json -Body $roadsNow 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;" } }'$body = @{ pathAlgorithmName = "DIJKSTRA"; maxResults = 1 cost = @{ vertexCost = "return (v) => 0.0;" edgeCost = 'return (e) => e.TryGetProperty<double>(out var w, "weight") ? w : 1.0;' }} | ConvertTo-Json -Depth 5Invoke-RestMethod -Method Post -Uri http://localhost:8080/path/11/to/12 -ContentType application/json -Body $body[ { "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).
Status codes
Section titled “Status codes”| 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).
See also
Section titled “See also”- Delegates: the no-query-language philosophy, the fragment contract, compilation, and
/delegates/validate - Stored queries: precompiled
Pathqueries invoked by name, no dynamic code required - Semantic traversal: the code-free
semanticsimilarity 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-surfaceexploitCost) - Plugins: how path algorithms are discovered
- Plugin registration: adding your own
Pathalgorithm, invocable by name throughpathAlgorithmName - Namespaces: per-namespace routing (
/ns/{ns}/…) - Source:
Algorithms/Path/,Delegates.cs