Who Inspected This Pump?
Match a connected pattern and preserve variable bindings across steps instead of accidentally combining unrelated records.
13 sections · 4 questions · 1 coding challenge
The arrow keys move between sections too.
A Query Could Describe the Connected Shape It Wanted
BOTIE
That is why the inspection variable matters. Once a row binds it to I1, the technician relationship must extend from I1. The drawing-like syntax makes this connection visible while leaving physical execution to the database.
Sources for this history
The Technician Is Two Relationships Away
The pump points to an inspection record. That inspection points to the technician who performed it. Botie must keep those connections together: a technician attached to another pump’s inspection is not an answer.
A pattern describes the connected shape we want to find. Its variables remember which node filled each position, so the next relationship continues from the same inspection rather than starting an unrelated search.
Invented maintenance records. Python exercises and workshop components expose the mechanism locally. Source-linked Cypher or SPARQL examples require the stated external database setup.
Step 1 of 1
Follow the same inspection through both links
The two pump histories are separate connected patterns.
Nodes and properties (6)
- P7 · Asset
- I1 · closed inspection
- T1 · Technician
- P8 · Asset
- I2 · open inspection
- T2 · Technician
- P7 to I1: HAS_INSPECTION
- I1 to T1: CARRIED_OUT_BY
- P8 to I2: HAS_INSPECTION
- I2 to T2: CARRIED_OUT_BY
Follow the bound inspection to its technician
Each expansion continues the same binding. The final distinct technician IDs therefore come from inspections attached to the requested asset.
Follow the bound inspection to its technician
A second pump’s technician must not enter the first pump’s result.
BOTIE
Result for these records
- 0T1
Follow the operations
1. Requested asset
P7
2. Start one pattern binding
[ { "origin": "P7", "current": "P7", "path": [ "P7" ], "edge_ids": [] } ]3. Asset, inspection and technician graph
Nodes and properties (6)
- P7 · Asset
- P8 · Asset
- I1 · Inspection
- status: closed
- I2 · Inspection
- status: open
- T1 · Technician
- T2 · Technician
- P7 to I1: HAS_INSPECTION
- P8 to I2: HAS_INSPECTION
- I1 to T1: CARRIED_OUT_BY
- I2 to T2: CARRIED_OUT_BY
Inspect the node and relationship records
{ "nodes": [ { "id": "P7", "kind": "Asset" }, { "id": "P8", "kind": "Asset" }, { "id": "I1", "kind": "Inspection", "status": "closed" }, { "id": "I2", "kind": "Inspection", "status": "open" }, { "id": "T1", "kind": "Technician" }, { "id": "T2", "kind": "Technician" } ], "edges": [ { "id": "h1", "source": "P7", "target": "I1", "type": "HAS_INSPECTION" }, { "id": "h2", "source": "P8", "target": "I2", "type": "HAS_INSPECTION" }, { "id": "t1", "source": "I1", "target": "T1", "type": "CARRIED_OUT_BY" }, { "id": "t2", "source": "I2", "target": "T2", "type": "CARRIED_OUT_BY" } ] }4. Read graph relationships
[ { "id": "h1", "source": "P7", "target": "I1", "type": "HAS_INSPECTION" }, { "id": "h2", "source": "P8", "target": "I2", "type": "HAS_INSPECTION" }, { "id": "t1", "source": "I1", "target": "T1", "type": "CARRIED_OUT_BY" }, { "id": "t2", "source": "I2", "target": "T2", "type": "CARRIED_OUT_BY" } ]5. Choose a relationship type
[ { "id": "h1", "source": "P7", "target": "I1", "type": "HAS_INSPECTION" }, { "id": "h2", "source": "P8", "target": "I2", "type": "HAS_INSPECTION" } ]6. Extend each binding by one relationship
[ { "origin": "P7", "current": "I1", "path": [ "P7", "I1" ], "edge_ids": [ "h1" ] } ]7. Choose a relationship type
[ { "id": "t1", "source": "I1", "target": "T1", "type": "CARRIED_OUT_BY" }, { "id": "t2", "source": "I2", "target": "T2", "type": "CARRIED_OUT_BY" } ]8. Extend each binding by one relationship
[ { "origin": "P7", "current": "T1", "path": [ "P7", "I1", "T1" ], "edge_ids": [ "h1", "t1" ] } ]9. Collect distinct identifiers
- 0T1
10. Result
- 0T1
These components operate on the supplied records in your browser. They do not run a graph database or interpret Cypher.
1. Make the starting entity specific
Bind a to the Asset whose stable ID is P7. The start is part of the question, not a detail to discard once expansion begins. A useful index can help locate it, but the query must still state the identity condition.
Our workshop binding keeps origin as P7 while current moves to the endpoint reached by each relationship. The path list is evidence of how that row was obtained.
A starting ID and a type condition prevent unrelated records from becoming candidates.
2. Extend one binding at a time
The first expansion binds i to an inspection connected to a. The second expansion uses that same i to find t. Several inspections can produce several valid binding rows.
If we instead collect every inspection and every technician independently, a cross-product can pair unrelated records. Reusing a variable expresses a shared entity, just as matching a key constrains a relational join.
Keep the connecting inspection ID visible while learning the pattern.
Inspect the first-hop inspection IDs
This program stops after the first relationship rather than answering the full technician question.
g = {"nodes": [{"id": "P7", "kind": "Asset"}, {"id": "P8", "kind": "Asset"}, {"id": "I1", "kind": "Inspection", "status": "closed"}, {"id": "I2", "kind": "Inspection", "status": "open"}, {"id": "T1", "kind": "Technician"}, {"id": "T2", "kind": "Technician"}], "edges": [{"id": "h1", "source": "P7", "target": "I1", "type": "HAS_INSPECTION"}, {"id": "h2", "source": "P8", "target": "I2", "type": "HAS_INSPECTION"}, {"id": "t1", "source": "I1", "target": "T1", "type": "CARRIED_OUT_BY"}, {"id": "t2", "source": "I2", "target": "T2", "type": "CARRIED_OUT_BY"}]}
print([e['target'] for e in g['edges'] if e['source']=='P7' and e['type']=='HAS_INSPECTION'])['I1']The next step must continue from I1, not from every inspection in the graph.
3. Read the Cypher pattern from left to right
MATCH describes the required graph shape. Variables a, i and t name the bound nodes; relationship types describe which links may connect them. RETURN DISTINCT t.id changes the output from one row per matching route to one row per technician.
DISTINCT is appropriate because the question asks which technicians, rather than how many inspections they performed. For an audit of individual visits, retain the inspection identity instead.
Projection and duplicate policy determine the report’s grain.
Run in your own environment
Find technicians through the bound inspection
The parameter supplies the starting asset without rewriting query structure.
In an empty Neo4j test database, run CREATE (a:Asset {id:'P7'}),(b:Asset {id:'P8'}),(i:Inspection {id:'I1',status:'closed'}),(j:Inspection {id:'I2',status:'open'}),(t:Technician {id:'T1'}),(u:Technician {id:'T2'}),(a)-[:HAS_INSPECTION]->(i),(b)-[:HAS_INSPECTION]->(j),(i)-[:CARRIED_OUT_BY]->(t),(j)-[:CARRIED_OUT_BY]->(u).
Supply asset as P7 using your driver’s parameter binding.
MATCH (a:Asset {id:$asset})-[:HAS_INSPECTION]->(i:Inspection)
-[:CARRIED_OUT_BY]->(t:Technician)
RETURN DISTINCT t.id AS technician
ORDER BY technicianT1 is returned. A second P7 inspection by T1 would still return that technician once.
Official documentation. Documentation checked .
Check yourself
Questions on Parts 1 to 3
Question 1 of 2
Which technician answers the P7 question?
Use the information below.
P7→I1→T1 and P8→I2→T2 are the only matching typed chains.4. Pass values through parameters
User-controlled asset IDs belong in driver parameters such as $asset. Do not paste them into a Cypher string. Keep query structure under application control and use reviewed templates for supported questions.
Dynamic labels, property keys and relationship-type syntax depend on the database version. If an application lets a user choose a structural option, validate it against a deliberate allowlist or use the documented supported parameter mechanism. String escaping is not a general substitute for that boundary.
Parameter binding preserves query structure; it does not authorise the requested asset.
5. Keep access rules on the path, not only the final result
A tenant restriction on the returned technician is insufficient if the match traverses another tenant’s inspection to get there. Restrict the starting node and every relevant intermediate entity and relationship according to the application’s permissions.
Use least-privilege database roles and an application authorisation layer. Hop limits and result limits control work; they do not grant permission to read a node.
The Python exercise uses a fully permitted fixture so the matching mechanism remains clear. The retrieval lesson adds an explicit scope before traversal.
6. Know which route repetitions the language permits
A two-hop pattern and a variable-length traversal have different scopes. Cypher has documented rules for reusing relationships in a match, and newer versions provide additional match modes. Do not assume an arbitrary Python loop exactly implements every Cypher mode.
Our workshop refuses to reuse a relationship already present in a binding’s route. It demonstrates this fixed connected pattern, with complete edge identities visible for inspection.
The lesson’s mechanism is a small specified subset, not a browser implementation of the query language.
Check yourself
Questions on Parts 1 to 6
Question 1 of 2
How should an application pass it to its approved query?
Use the information below.
A user types an asset ID containing quotes.Try it with Botie
Build with Botie
Write and run Python