Slack Isn't in the Flight, It's in the Hand-off: How a Graph Database Tames Knock-on Delays

Model flight knock-on delays as a graph, using edge slack and zero-row invariants so agents can safely explain and validate scheduling decisions.
Starting from a counterintuitive modeling decision: the main source of flight delays is knock-on propagation, not point failures, and slack belongs to the hand-off edge between two flights, not the flight node itself. The author uses `NEXT_LEG` and `CREW_NEXT` edges to capture the physical dependencies of aircraft and crew, so delay propagation can be relaxed to a fixed point with a single Cypher query. This yields three capabilities: answering "why is this flight late" directly with the shortest path (provenance comes free with traversal); validating any cancellation, aircraft swap, or crew swap before commit with four "must return zero rows" invariants; and evaluating plans in a sandbox via `GRAPH.COPY` forks, where only a validated `decision_id` can be committed. The core stance is "the graph decides, the model only talks, the human confirms," guarded by a test that fails entirely without the graph.
When a flight is delayed, our first instinct is often "this flight has a problem." But according to CODA Digest data from Eurocontrol, Europe's air traffic management organization, 44.6% of delay minutes in Q2 2023 were "knock-on delays" rather than primary delays. The U.S. Department of Transportation's September 2025 Air Travel Consumer Report also lists "late-arriving aircraft" as the single largest cause of delays.
Knock-on delay isn't a new problem happening now — it's an old problem "arriving." An aircraft flies 4 to 6 legs per day, and the same crew operates a portion of them. The second leg waits for the first. This dependency is physical, real, and naturally forms a graph.

Indian regulators' statements after IndiGo's December 2025 operational collapse made the issue clear: 2,507 cancellations, 1,852 delays, over 300,000 passengers stranded for three days, and a fine of 2.22 billion rupees. The Press Information Bureau's investigation concluded that scheduling "maximized crew utilization," cut scheduling buffers, and increased reliance on deadheading, tail swaps, and minimum recovery times. Maximizing utilization means minimizing redundancy.
Modeling Decision: Slack Belongs to the Edge, Not the Node
The author's first instinct was to add a slack property to each flight node, but that's wrong. Slack doesn't belong to a flight — it belongs to the "gap" between two flights:
slack_min = scheduled_ground_time − minimum_legal_turnaround
The flight itself isn't fragile; the "hand-off" is. So the model puts all the work into two edge types:
| Edge type | Meaning | Properties |
|---|---|---|
NEXT_LEG | The immediately following leg of the same aircraft | min_turn, sched_ground, slack_min |
CREW_NEXT | The immediately following leg of the same crew | min_rest, sched_ground, slack_min |
Once Slack is placed on the edges, knock-on propagation becomes a query you can write down. Delay spills from flight a to flight b exactly when the former exceeds the buffer between them:
MATCH (a:Flight)-[e:NEXT_LEG|CREW_NEXT]->(b:Flight)
WHERE a.status = 'SCHEDULED' AND b.status = 'SCHEDULED'
AND (a.delay_min - e.slack_min) > b.delay_min
WITH b, max(a.delay_min - e.slack_min) AS new_delay
SET b.delay_min = new_delay,
b.proj_dep = b.sched_dep + new_delay,
b.proj_arr = b.sched_arr + new_delay
RETURN count(b) AS changed
Execute repeatedly until changed = 0 — that's Bellman-Ford relaxation to a fixed point, done inside the database. The max() aggregate is crucial: a flight has two inbound dependencies (aircraft and crew), and it must take the worse one because it has to wait for both. One aggregation, and the rule is expressed only once.
Bellman-Ford relaxation: Bellman-Ford is the classic single-source shortest-path algorithm, and its core operation is "relaxation" — repeatedly checking each edge, and updating the target vertex's value if going through that edge yields a shorter (or better) distance. Since each iteration converges at least one vertex, at most |V|−1 rounds are needed to reach a stable solution. This article borrows that idea: treat "delay" as the path cost, treat slack_min on each NEXT_LEG/CREW_NEXT edge as that edge's "capacity/threshold," and propagate across all edges once per round until no flight's value changes anymore (changed = 0) — i.e., "relaxation to a fixed point." Unlike the classic algorithm, the update here isn't a minimization but a max() aggregation over multiple predecessors — because a flight must wait for both the aircraft and the crew dependency, it must take the worse one. This also explains why propagation is naturally monotonic (delays only increase), and the "forks must be reset" point mentioned later is a direct consequence of that monotonicity.
Minimum legal turnaround and minimum rest (min_rest): These two parameters are hard operational constraints in airline scheduling that determine the "legal" minimum interval between two flights. Minimum turnaround is the shortest time required after an aircraft lands and before its next departure to complete deplaning, cleaning, refueling, catering, inspection, boarding, and other ground handling — typically varying by aircraft type, airport, and whether the flight is international. Minimum rest applies to crew: the legally required rest period between two consecutive duty assignments for the same crew, often far longer than turnaround time and governed by Flight Time Limitations (FTL/FDTL) regulations. Neither equals "scheduled ground time" (scheduled_ground_time) — the interval actually allocated during scheduling. slack_min = scheduled_ground_time − minimum_legal_turnaround therefore measures how much buffer remains relative to the regulatory floor; the thinner the buffer, the more easily upstream delays spill downstream. The bug mentioned in the article — "tightening the hand-off to min_turn when min_rest is the longer requirement" — arises precisely because aircraft constraints and crew constraints must be checked separately, taking the stricter of the two.
Why "Why Is This Flight Late" Is the Real Feature
The author considers the most showcase-worthy query to be one that answers "why is it late" with a single path:
MATCH p = (root:Flight {id: $root_id})-[:NEXT_LEG|CREW_NEXT*1..8]->(target:Flight {id: $target_id})
RETURN [n IN nodes(p) | n.id] AS chain,
[r IN relationships(p) | type(r)] AS hop_types,
[r IN relationships(p) | r.slack_min] AS slack_at_each_hop,
length(p) AS hops
ORDER BY hops ASC
LIMIT 1
It answers with a path: 6E101 was hit by smog and handed the aircraft to 6E117 with only 5 minutes of margin, which in turn passed the delay to 6E131 through an 8-minute margin. No model wrote that sentence — it was a traversal. If the explanation is wrong, the graph is wrong, and you can go back and inspect the graph.
The author believes this is the most underrated use of graph databases in agent systems: people always treat them as a retrieval layer, pulling some context to stuff into prompts. But the traversal result is itself structurally an explanation — provenance comes for free, because the answer is the path you walked.
Proving Safety with "Zero Rows"
The agent can propose actions: cancel a pair of flights, swap in a spare aircraft, swap in a backup crew. Before any action is committed, four invariants must hold, each a query that "must return an empty set":
- INV-1: No crew's duty window exceeds the limit
- INV-2: No turnaround is shorter than the minimum
- INV-3: Tail continuity — the aircraft's arrival airport equals the next leg's departure airport
- INV-4: Every flight has a terminal state
INV-3 deserves elaboration because the initial design missed it. Cancelling a delayed flight is the most intuitive containment action, but it's often badly wrong: cancelling a middle leg leaves the aircraft in the wrong city for the rest of the day. Aircraft can't teleport. So cancellations must be done as "round-trip pairs" or paired with an explicit ferry flight.
The author discovered this by writing the invariant and watching the plan fail. This is exactly the reason to express safety as "the absence of a pattern" rather than application logic: the absence of a matching path is a proof of safety, and a query that returns zero rows is far harder to fool yourself with than an if statement you forgot to write.
Fork Before Touching the World
Every plan is evaluated on a GRAPH.COPY fork:
fork → apply plan → re-propagate knock-on → check 4 invariants → score → drop fork
FalkorDB's GRAPH.COPY clones nodes, relationships, properties, indexes, and constraints, while the source graph remains readable and writable. The fork is dropped in a finally block, including when an exception is thrown internally.
The author also confesses a bug: relaxation is monotonic and only raises delays. The first version of the evaluation loop applied cancellations on the fork and then re-ran relaxation, so every plan scored the same. The reason is obvious: cancelling a flight can't lower delays that have already propagated downstream. Forks must reset to the published schedule and re-propagate from the perturbation point after the action is applied. Obvious in hindsight, completely invisible until the scores came out flat.
GRAPH.COPY and copy-on-write-style sandboxing: GRAPH.COPY is a graph-level clone command provided by FalkorDB that copies all nodes, relationships, properties, and index and constraint definitions of the source graph into the target graph. The key point is that the source graph remains readable and writable after cloning, and the two are independent, so it can be treated as a "world-state snapshot." In this article's planning flow, each candidate plan applies its action on an independent clone graph, re-propagates knock-on delays, runs the four invariant checks, and scores; the fork is dropped whether it succeeds or fails. This aligns with the "rollback" idea of database transactions or the "immutable data structure + derive a new version" approach in functional programming: the real world (the published schedule) is not polluted by tentative operations, and only plans that pass validation and obtain a decision_id are committed back to the main graph. Note that cloning incurs space and time overhead; at large graph sizes frequent forking becomes a bottleneck, which is why each fork is cleaned up promptly in finally.
The Constraint That Shaped the Architecture
Borrowing a hackathon rule, the author set an iron law for himself:
The graph decides. The model only talks. The human confirms.
In the MCP server, eight of the nine tools are read-only. The ninth is commit_plan, which accepts only a decision_id and never the plan body. That signature is the entire safety argument: the model cannot invent actions out of thin air because there is no parameter through which to pass an action. It can only point to something the graph has already validated and recorded. Every Decision is stamped with the graph version it was validated against; if the live graph has changed, the commit is rejected. A plan proven safe for one world state is not a proof of safety for another.
MCP (Model Context Protocol): MCP is an open protocol proposed by Anthropic for connecting large language model applications to external tools and data sources in a standardized way. It uses a client-server structure: the application hosting the model acts as the client, communicating with the MCP server over JSON-RPC, and the server exposes a set of "tools," "resources," and "prompts" to the model. The MCP server in this article is exactly such a tool provider, exposing nine tools for the agent to call. The significance of MCP is that it decouples "what the model can do" from "what the system permits": the tool's parameter schema is itself a permission boundary. commit_plan in the article accepting only decision_id leverages exactly this layer — the model can call the tool but cannot inject an unvalidated action through parameters, making the interface signature an auditable safety contract.
If You Can Keep Only One Test, Keep This One
tests/graph-off.test.ts points the product at a dead database and asserts that every path fails. No cache, no defaults, no degraded mode, no in-memory fallback. It also greps the source code for forbidden data-store imports. If the product still works with the graph removed, the graph was just decoration.
This test also let the author add MongoDB for user accounts without weakening the core claim: accounts are application shell; flights, edges, knock-on effects, plans, decisions, and receipts are the product. packages/graph is forbidden from importing mongoose, enforced by the linter rather than good intentions.
Things I'd Tell Myself If I Started Over
Write the oracle first. Before connecting to the database, the author wrote a pure TypeScript checker that validated generated schedules against the same four invariants as the Cypher. It immediately caught three real bugs: negative crew rest margin (tightening the hand-off to min_turn when min_rest is the longer requirement), rotations crossing the 23:00 day boundary, and over-splitting of crews. An independent implementation of the same rule is more valuable than another test of the same implementation.
Put all queries in one file. All Cypher lives in queries.ts, never inlined in routes, scripts, or tests. This makes the "queries this product depends on" section of the README copy-pasteable, and lets the queries be reviewed as a set.
Name your relationships. FalkorDB only does existence checks on unnamed relationships. MATCH (a)-[e:NEXT_LEG]->(b) behaves correctly when e is actually used; drop the alias and the count can silently go wrong.
What This Is and Isn't
This is a decision-support prototype built on synthetic data. Every flight, tail number, crew, and passenger count is generated; only airport codes and coordinates are real.
It is explicitly not a replacement for an operational solver. Lufthansa's OPSD is a mature deployed system; INFORMS reported that dispatchers accepted 26% of its recommendations, roughly twenty developers spent four years on it, and most of that time went into data preparation. 26% is a published number; why it's 26% is the author's hypothesis rather than a finding — but that hypothesis is exactly the project's foothold: operational dispatchers bear personal responsibility for decisions, and a recommendation you can't interrogate is a recommendation you can only accept on faith.
So the stance isn't "better plans" but: see the whole chain, explain every recommendation as a path, and test it against hard constraints before anyone acts. Containment option selection is a scoring heuristic with declared weights, not a proof of optimality. The invariants only cover what is modeled — bad input data can still produce plans that pass validation. duty_limit_min = 780 is a placeholder, not a regulatory number; reading the actual DGCA FDTL CAR is still on the to-do list.
Related articles

Workbuddy for Absolute Beginners: How a Desktop AI Agent Fits Into Real Workflows
A beginner's guide to the Workbuddy desktop AI agent: installation, claiming credits, file management, and building automated workflows—compared with CodeX for fit and cost, helping professionals shift from AI question-askers to AI managers.

OpenAI's New Agent Stack: Computer Use, Decisions API, and Ultra-Fast Workflows
OpenAI Dev Day unveiled a new agent stack: Computer Use outperforms humans, Decisions API delivers ultra-low-latency classification via Luna, Agents API opens the same Computer Use capabilities, and GPT-6.1 Sol costs only one-seventh of Astra. Core updates, benchmark data, and developer insights.

Liang Wenfeng Open-Sources DeepInfo Inference Framework, Takes On U.S. Ban
DeepSeek open-sources its DeepInfo inference framework after its models, featuring GPU/CPU cross-pipelining, small-model prediction with large-model verification, and full Huawei Ascend and NVIDIA chip support—facing the U.S. federal ban head-on. This article analyzes its significance for AI deployment and open-source strategy.