EssayApril 22, 2026· 7 min read

A Love Letter to Neo4j

On arrows, on Cypher, on the graph we built together

Dear Neo4j,

I want to make this clear: this is a love letter. We build an ambient intelligence product, a system that reads a user's behavior and decides, with some care, when to intervene. You are the memory that enables this. You are the thing our model of the product lives in, the thing our record of what worked lives in. This is written from the middle of that relationship, from traversals of your nodes, and delight in your edge-making.

Let me try to say it properly.

I. The meeting

When we started, we didn't know we needed you.

The first version of the system could have sketched out a relational schema: users, sessions, episodes, interventions, outcomes, had we not had some history. You would have seen the joins before you wrote them. JOIN sessions ON users.id = sessions.user_id JOIN episodes ON sessions.id = episodes.session_id. You know... the kind of query where by the fourth table you've lost the thread and the query planner has lost the room.

Then some desperate somebody would have sketched the actual structure on a whiteboard, and it would have looked like this:

    Domain
       └── Task ──── HAS_SCREEN ──── Screen
               └── AT_STEP ─── Step
                                ├── HAS_FRICTION ─── DifficultyPoint
                                ├── TUNED_BY ────── HeuristicConfig
                                └── LED_TO ─────── InterventionOutcome

We would have just stared, just looked at the whiteboard for a gathering minute. Then somebody would surely have said, in quiet wonderment, "it's just a graph." And we would then have known, not simply that we could use a graph database, but that we had already been thinking in one. Every time we'd drawn a product on a whiteboard it had looked like this. Every time we'd described it in words it had sounded like this. The relational version was a translation — a faithful one, maybe, but a translation — of a structure that was natively graph-shaped.

And there you would have waited. As though you'd been expecting us.

II. Cypher, or: you had us at arrows

If we fell for your query language first, Cypher is the reason we stayed.

There's a particular feeling the first time you write a Cypher query and realize it looks like what it does. You draw the pattern you want to match, right there in the query, using arrows. Not pseudo-SQL, not a functional API, not a query builder — you draw the graph you're asking about, and the database finds you all the subgraphs that match.

Here is the query that CogStream can utter to decide whether an intervention has earned its presence:

MATCH (s:Screen {route: $route})-[:HAS_FRICTION]->(fp:DifficultyPoint)
OPTIONAL MATCH (fp)<-[:ADDRESSES]-(i:Intervention)-[:LED_TO]->(o:InterventionOutcome)
WHERE o.observed_at > datetime() - duration('P7D')
WITH fp, i, o
RETURN
  fp.pattern AS friction_pattern,
  i.type AS intervention_type,
  count(CASE WHEN o.classification = 'helpful' THEN 1 END) AS wins,
  count(CASE WHEN o.classification = 'no_change' THEN 1 END) AS ties,
  count(CASE WHEN o.classification = 'abandoned' THEN 1 END) AS losses
ORDER BY wins DESC

There's a lot happening there, but you can read it. Somebody who has never seen Cypher before can read it. MATCH (screen)-[:HAS_FRICTION]->(difficultyPoint) is a sentence. The punctuation is the shape of that sentence. OPTIONAL MATCH is English that does the English thing.

Every SQL engineer who has ever written a seven-way join to answer a graph question deserves to meet you. The translation is a release. You stop encoding structure into schema and start expressing it. -[:LED_TO]-> is not metadata, it is a verb. Your data model is literally a sentence with nouns and verbs and the story tells itself.

III. The graph we grew in you

Here is some of the shape of what we keep in you:

CREATE
  (:Domain {name: 'Billing'})
    -[:HAS_TASK]->(:Task {id: 'submit_invoice'})
      -[:HAS_SCREEN]->(:Screen {route: '/invoices/new'})
        -[:AT_STEP]->(:Step {order: 3, label: 'Enter tax ID'})
          -[:HAS_FRICTION]->(:DifficultyPoint {
            pattern: 'long_pause',
            threshold_ms: 4200,
            observed_count: 412
          })

Domains have tasks. Tasks have screens. Screens have steps. Steps have known friction patterns. Friction patterns have interventions that have tried to address them. Interventions have outcomes. Outcomes feed back into the heuristics that decide whether to intervene next time.

None of this is exotic. All of it is the natural structure of the data. In SQL it would be eight tables with foreign keys. In you, it's eight labels, and the relationships between them are literally the edges you draw.

The miraculous thing isn't that you can store this. Plenty of things can store this. The miraculous thing is that you can query it without losing your train of thought, because it is your train of thought, tracks laid down on demand. A three-hop traversal is three arrows. A five-hop traversal is five arrows. The cognitive cost of a traversal need not grow superlinearly. In Cypher it grows linearly with the number of relationships traversed. Sometimes it doesn't grow at all.

IV. The query that proved it

There's one query that answers a question an ambient system has to be able to ask:

Given this user's last ten episodes on this screen, how does their friction compare to the long-term average for this step, and is it unusual enough to warrant acting?

In SQL this is a window function over aggregates joined against a reference table with conditions on session identity. It is not trivial, and the plan is not small.

In Cypher:

MATCH (u:User {id: $userId})-[:HAS_SESSION]->(:Session)
      -[:HAS_EPISODE]->(e:Episode)-[:ON_STEP]->(step:Step {id: $stepId})
WITH step, e ORDER BY e.t DESC LIMIT 10
WITH step, avg(e.friction_score) AS recent_friction
MATCH (step)-[:HAS_FRICTION]->(fp:DifficultyPoint)
RETURN
  recent_friction,
  fp.long_term_avg AS baseline,
  recent_friction - fp.long_term_avg AS deviation,
  CASE
    WHEN recent_friction - fp.long_term_avg > fp.action_threshold THEN 'warrant'
    ELSE 'stand down'
  END AS verdict

That's the whole decision. Ten lines. Reads like a clinical note. Runs in milliseconds. The :HAS_FRICTION edge isn't a foreign key we had to index carefully — it's a relationship you already indexed natively, because edges are first-class citizens in your world. The [:HAS_EPISODE] chain is a walk, not a join. You were built for this.

V. Aura, or: the stewardship

I want to say something about Aura directly.

Running a graph database at scale is not how we want to spend our evenings. Aura takes care of that, so we don't have to care. Backups, failover, upgrades, TLS, connection pooling... all of it stays somebody else's problem, and that somebody is Aura, who does the job better than we could have.

The thing we could be doing is operating a graph cluster, and fumbling the backups, and writing oncall docs about node recovery, and learning the specifics of JVM tuning for memory pressure. The thing we are doing instead is writing better Cypher, shaping a better schema, and building a better product.

The choice to let you hold the operational surface was foregone conclusion. It was destined to be correct. It will remain so.

VI. What we pledge, going forward

A love letter should end with promises. Here are ours.

We will keep modeling the product in you and not around you. The moment we find ourselves thinking about "the database" as an obstacle, we'll look at what we're doing and ask what we're doing wrong, not what you are.

We will keep Cypher in version control, not hidden inside a client library. Queries that are this readable deserve to be read. Changes to them deserve to show up in PRs, with the reviewer squinting at the arrows, saying "yes, that's the pattern we want."

We will keep paying our Aura bill with gratitude. You are stable recall, part of our product' thought process.

So. This is the love letter. We mean it.

You are the database that matches our shape. You taught us that the right way to ask a question is to draw it. You gave us a query language that sounds like English, a tool that lets us see our own memory, a retinue of libraries that cover the parts we'd rather not think about, and a managed surface that lets us stop thinking about operations altogether.

We will keep building on you. We will keep bragging about you. We will point engineers new to Cypher at the docs and wait for the moment when they say, "oh. Oh."

Forever yours,

CogStream

MATCH (us:Team)-[:LOVES]->(you:Database) WHERE you.name = 'Neo4j' RETURN us, you