Follow what makes the next event happen
Programming as Causal Graph Construction

What It Is
Programming connects causes to effects inside a bounded system. A click runs a handler, a handler changes state, and that change redraws the screen. Thinking of those connections as a causal graph helps you find why an expected result did not occur.
The same debugging skill transfers to behavior. A race condition and competing morning routines have identical causal structures: two processes try to determine what happens next without a defined order.
Note: This is a mental model that has proven useful for debugging (N=1, Will's practice), not a claim about how programs or brains "actually work." The question is: does viewing systems through causal graphs help YOU debug more effectively?
A closer look
Trace a consequence
To debug the sequence, find the connection that failed or the condition that allowed an unintended action.
Read this diagram
An event occurs → A condition is checked → An operation runs → Its consequence follows.
The Core Insight: Data Flow vs Causality
The familiar data-pipeline model follows input through transformations to output. That explains pure functions and pipelines well. Event-driven systems also require you to track when something happens, what state it changes, and which other actions it enables or prevents.
Pattern matching is the fundamental computational mechanism behind each edge: when a cause matches a pattern, its associated transformation occurs. Programming paradigms organize these rules differently.
| View | Mental Model | What It Explains Well | What It Misses |
|---|---|---|---|
| Data Flow | Input → Transform → Output | Pure functions, pipelines, ETL | Event handlers, race conditions, cleanup, prevention |
| Causal Graph | Event → Causes → Effects (with time) | Callbacks, concurrency, lifecycle, state changes | Works for both simple and complex cases |
A pipeline is a simple causal graph. The causal view therefore covers both that case and more complex arrangements that data-flow thinking does not explain.
What Data Flow Misses, Causality Captures
Timing and control matter even when no data is being transformed:
| Pattern | Data Flow Explanation | Causal Graph Explanation | Behavioral Parallel |
|---|---|---|---|
| Debouncing | "Delay the data" | Recent causes cancel pending causes | Gym intention gets canceled by couch-sitting cause |
| Initialization order | "Data dependencies" | Causal prerequisites (A must cause B before C can occur) | Can't execute work until coffee causes alertness |
| Mutual exclusion | ??? (no data transform) | One causal path blocks other paths from executing | Can't watch TV AND do deep work (mutually exclusive causes) |
| Prevention/Guards | ??? (no data transform) | Blocking causal paths before they execute | Prevention blocks bad habits vs resisting them |
| Cancellation | ??? (no data transform) | Active negation of pending causal chains | Aborting evening-doom-scroll before it starts |
| Cleanup/Disposal | ??? (no data transform) | Causal consequences of termination | Gym habit requires shower/meal routine cleanup |
For these patterns, following causes makes time, state, and control flow clearer. Following data remains useful for pure, stateless transformations.
Flow-Based Programming and Compute-Current
Continuation-Passing Style (CPS) makes the next computation explicit. Flow-Based Programming makes the locations and channels of computation explicit. Together they describe temporal and spatial causality: what happens next and where it happens. The term compute-current describes computation moving through stable patterns.
CPS vs Flow-Based Programming
| Paradigm | What It Reveals | Mental Model | Causality Type | Physical Analogy |
|---|---|---|---|---|
| CPS | What pattern matches next | Relay race of pattern matches | Temporal (sequential) | Quantum state transitions |
| Flow-Based | Where data/compute flows | River system of transformations | Spatial (concurrent) | Water through landscape |
| Data Pipeline | Pure transformations | Assembly line | Neither (abstraction) | Factory process |
In CPS, a computation receives an explicit continuation: f(x, nextPattern) → passes result to nextPattern. The continuation is the next pattern to match. Hidden returns and the implicit stack are removed, leaving each state to specify which computation follows it.
In Flow-Based Programming, stable nodes connect through data channels. The paths are explicit, and data is transformed at the node where it arrives. Stable patterns guide the moving compute-current.
Both expose computation as causality, expressed either as sequential transitions or as flow through space.
Data Location = Computation Location
Where data is located determines where computation occurs. A pattern is matched where it exists; moving the data moves the compute-current. Stable nodes provide places to retain information and transform it.
In a river, water's location determines whether it enters rapids, slows in a pool, or follows a bend. Rock formations and the riverbed shape what happens to the flow. Its movement is the work, so its location cannot be separated from what happens to it.
// Flow-based: Data location = compute location
inputStream
.pipe(transformA) // Compute happens AT transformA node
.pipe(transformB) // Now compute happens AT transformB node
.pipe(output) // Finally compute happens AT output node
// Data physically moves through space
// Compute-current follows the data
Attention and working memory provide the corresponding location in behavior. Attention on a phone sends computation through social and distraction patterns. Attention on code sends it through programming patterns. Moving attention moves your personal compute-current.
Compute-Current as Physical Flow
Compute-current moves physically through stable patterns, as electricity moves through circuits, water through a landscape, or chemical gradients through cells. Code supplies patterns that guide the flow; memory supplies patterns that hold it. Execution occurs when current flows through those patterns, and data movement physically moves that current.
This explains several design constraints. Moving data costs resources because it moves the current. Locality matters because distant locations require longer paths. Caching keeps information near where computation happens, and architecture determines how efficiently flow moves between its stable structures.
Electron flow through molecules, neural signals through brain architecture, and chemical gradients through cellular machinery are all instances of compute-current moving through stable patterns.
Why This Matters Practically
Prevention architecture blocks a path before current enters it. Removing the path avoids having to stop an active flow:
// Prevention: Remove the flow path
// (no cookies in house = no temptation current can flow)
// Resistance: Try to stop flowing current
// (cookies present, try to resist = fight the current)
In state-machine design, states are stable pools where current can remain, transitions connect them, and default scripts provide the easiest outgoing paths. Activation cost is the energy required to move through a new channel.
For working memory, attention is the compute-current of consciousness. Its limited capacity prevents it from flowing in many directions at once. It follows low-resistance defaults and salient triggers, and redirecting it costs energy. Discretization divides the flow into smaller segments that can be managed separately.
Environment design changes the available paths. A phone in the bedroom provides a route toward distraction. A guitar by the couch provides one toward practice. These default paths determine where behavioral current naturally goes.
Comparing Causal Views
| View | What It Reveals | Physical Analogy | Best For | What It Misses |
|---|---|---|---|---|
| CPS | Temporal causality (what next) | Relay race | Sequential processes, control flow | Spatial/concurrent patterns |
| Flow-Based | Spatial causality (where flows) | River system | Concurrent/parallel, data movement | Precise sequencing |
| Data Pipeline | Pure transformations | Assembly line | Stateless functions | Events, state, concurrency |
| Causal Graph | Complete causal structure | Neural network | Debugging, understanding complexity | Implementation details |
CPS and Flow-Based Programming together supply the complete temporal and spatial account. Both remove abstractions to expose causality: one follows sequential transitions, the other movement through space. Computation is that causality moving through stable patterns.
Integration with Causal Graph Framework
The causal graph supplies the topology. Its nodes represent states, events, and conditions. Its edges specify what causes what, including which causes fire when.
Flow-Based Programming adds questions about location: where is the data or compute-current, which paths can it take, and which stable patterns will it encounter? CPS adds questions about sequence: which pattern matches next, in what order do transitions occur, and which continuation takes control?
These three views describe structure, space, and time in the same system. Together they explain how causality propagates through computation.
The Data Pipeline Breaks Down
Pattern 1: Event Handlers
Consider this code:
let clicked = false;
button.onClick(() => { clicked = true });
if (clicked) {
console.log("Button was clicked!");
}
Following only the data suggests that the handler sets clicked and the if reads it. But the code prints nothing. The if runs before a click can occur. It reads the current state once and moves on; it has not subscribed to future changes.
The behavioral version is relying today on “I'll want to go to the gym tomorrow morning.” That future motivated state may never occur. Current behavior cannot run on a future cause, just as the current if cannot detect a future click.
The code needs a subscription that runs the effect when the event occurs:
button.onClick(() => {
console.log("Button was clicked!");
});
The behavior needs a causal path established now. Arrange the environment so going to the gym follows by default, without depending on future motivation.
Pattern 2: Race Conditions
counter = 0
# Thread 1
counter = counter + 1
# Thread 2
counter = counter + 1
As arithmetic, this appears to take the counter from 0 to 1 to 2. But both threads can read 0, calculate 1, and write 1. The result can be 1 or 2 because two causal chains modify the same state without a defined temporal order.
Two morning scripts can compete in the same way:
- wake_up → coffee → deep_work
- wake_up → coffee → friend_calls → conversation
If both are available, neither has defined precedence. Whether work begins depends on the circumstances of that morning.
A lock or atomic operation establishes ordering in code:
with lock:
counter = counter + 1
For behavior, specify which state transition takes priority, or use environment design to make the scripts mutually exclusive.
Pattern 3: Missing Cleanup
A React effect creates a resource and returns the operation that disposes of it:
useEffect(() => {
const subscription = subscribe();
return () => subscription.unsubscribe();
}, []);
Data-flow thinking does not explain this lifecycle. Mounting the component causes a subscription to exist; unmounting causes its disposal. Without cleanup, the subscription persists after the component that created it has ended.
A gym habit also creates consequences. Exercise depletes energy, creates hunger, requires a shower, and takes time. If the routine has no post-workout meal or shower sequence, those consequences accumulate as resistance to the next session.
Return cleanup functions from effects that create resources. In behavior, identify every consequence and provide a script to handle it. A lasting habit needs the complete chain, including what happens after its trigger succeeds.
Observable Patterns in Systems
The same causal structures appear in code and daily life.
Observable Pattern 1: Temporal Coupling
Correctness depends on doing things in a particular order:
db.connect() # Must happen first
db.query(...) # Requires connection
db.disconnect() # Must happen last
For many people, work requires coffee first: wake → coffee → work. A social plan after the gym requires a shower first: gym → shower → social. Arriving sweaty breaks the expected social sequence.
Ask which prerequisites must occur before the intended action can succeed.
Observable Pattern 2: Causal Cancellation
A new cause can cancel a pending effect. In debouncing, each keystroke removes the previous search timer and starts another:
// New keystroke cancels previous search timer
clearTimeout(timer);
timer = setTimeout(search, 300);
A phone notification cancels deep work. Sitting on the couch cancels the intention to leave for the gym. YouTube autoplay cancels the evening project session.
Find which event cancels the intended chain, then block that event before it does so.
Observable Pattern 3: State Machine Transitions
Current state determines which causes can produce effects:
if (state === "logged_in") {
// These causal paths only available in this state
allow(post_content);
allow(view_dashboard);
}
In a behavioral state machine, “home_from_work” makes couch, TV, and phone the defaults. “At_gym” makes working out and showering available. Reaching the workout from the couch requires paying the cost of a state transition.
Ask which state you occupy, which transitions it permits, and what activation cost is required to reach the needed state.
Debugging with Causal Graphs
The debugging procedure is identical for code and behavior.
Step 1: Identify Expected Causal Chain
Write the sequence that should occur. In the interface: user clicks → handler runs → state changes → UI re-renders. In the morning: alarm rings → wake up → coffee → shower → work.
Step 2: Observe Actual Execution
Log each step in code:
console.log("Button clicked");
console.log("Handler executing");
console.log("State updated:", newState);
console.log("UI rendering");
Track the actual behavioral sequence with the same care. The alarm rang, then snooze ran, then phone scrolling began in bed. Coffee was never reached. Snooze was an unexpected cause and the phone supplied a competing one.
Step 3: Find Broken Causal Link
If state changed but the UI did not re-render, inspect the connection between them. If the alarm rang but work never started, inspect the competing path that won: snooze → phone → doom-scroll.
Step 4: Repair the Causal Graph
Add an observer for a missing subscription, a lock for a race, or disposal logic for missing cleanup.
For behavior, remove competing triggers, such as the bedroom phone. Add transition scripts where prerequisites are missing. Where activation is too expensive, lower the threshold through repetition.
Framework Integration
Connection to State Machines
A state machine can be understood as a causal graph with time. States are nodes, transitions are causal edges, and the current state restricts which edges can run:
class WorkflowMachine {
state = "idle";
trigger(event) {
// Current state gates which causes can succeed
if (this.state === "idle" && event === "start") {
this.state = "running"; // Causal transition
}
}
}
Coming home from work enters a “home_evening” state with default paths such as couch → TV → phone. Modeling those paths makes the problem inspectable. Running a different sequence requires energy to change states.
Connection to Prevention Architecture
Prevention blocks a causal path before it runs:
// Prevention: Don't let cause execute
if (userIsBanned) return; // Block causal path
// Resistance: Let cause execute, try to resist effects
executeUserAction(); // Cause fires
if (shouldReject) undo(); // Try to undo effects (expensive!)
Keeping cookies out of the house removes a path to eating them. Keeping them present and trying to resist consumes resources. Removing an edge is cheaper than fighting the process once it is running.
Connection to Cybernetics
A cybernetic loop is a causal feedback circuit:
Action → Observe Result → Compare to Goal → Adjust Action
↑ ↓
└──────────────────────────────────────────────┘
Each arrow causes the next step, and the circuit produces the system's behavior. PID controllers, retry logic, and adaptive algorithms use this pattern. So does noticing a missed gym session, adjusting the morning routine, and testing a new trigger.
Connection to Question Theory
Questions can trigger a causal search. Asking “What is the mechanism?” asks for the causal structure. It causes you to identify states, events, and conditions; trace their connections; locate broken links; and propose repairs. The question itself starts the debugging process.
Connection to Computation as Core Language
Computation can be described in these terms. State is the current configuration of the graph. Execution propagates effects along its chains. Functions package input-to-output relationships; loops repeat patterns; conditionals select which path runs.
Programming languages provide notation for these graphs. Functional languages emphasize pure causality, imperative languages sequential causality, and reactive languages event-driven causality. Each describes cause and effect through different features.
Practical Applications
Application 1: Debugging Behavior as Debugging Code
Start by logging the difference between the intended and actual routine:
Expected: wake → coffee → work
Actual: wake → phone → scroll → guilt
Then ask why the phone's trigger fired first. It sits visibly on the nightstand, while coffee requires getting up and crossing a higher activation threshold.
Change those connections: remove the phone, put the coffee maker on a timer, and allow the phone only after coffee. Test the arrangement for 7 days, observe whether coffee now starts reliably, and adjust what still fails. This is identical to debugging a race condition or event-handler bug.
Application 2: Designing Systems (Code and Life)
Make the desired sequence the default. A resource whose release depends on the programmer remembering it is easy to leak:
// BAD: Require explicit cleanup (high failure rate)
const resource = acquire();
// ... developer must remember ...
release(resource); // Often forgotten!
// GOOD: Cleanup is automatic consequence
useResource(() => {
// Use resource
}); // Cleanup happens automatically
A daily gym decision introduces the same kind of dependence on remembering and choosing:
BAD: Gym requires daily decision (high resistance)
morning → should_I_gym? → (usually no)
GOOD: Gym is default cause
morning → gym_clothes_preset → automatic_drive → gym
Preset clothes and the established trip make going the path of least resistance.
Application 3: Understanding Interference
Two asynchronous operations can overwrite the same state:
async function processA() {
const data = await fetch("/a");
state.value = data; // Might overwrite processB's write
}
async function processB() {
const data = await fetch("/b");
state.value = data; // Might overwrite processA's write
}
A deep-work routine from 8-10am conflicts with calls and messages scheduled in the same period. Both require the same time and attention, so both cannot execute successfully in that state space.
In code, serialize access or give the operations independent state:
state.a = dataA; // Separate state spaces
state.b = dataB;
For behavior, reserve work for 8-10am with social access blocked, and social time for 6-7pm with work blocked. Separate contexts can do the same thing: an office without social access and a cafe without work access.
How to Practice Causal Thinking
When code fails, write its expected chain and find where execution diverged. Apply that procedure when behavior fails: alarm → coffee → work was expected, but alarm → snooze → phone → guilt occurred. Identify the competing phone trigger.
Practice transferring the diagnosis explicitly. After debugging a race or event handler, name it as competing causal chains with undefined ordering, then look for the same arrangement in daily routines.
Test whether diagramming a morning routine as you would an asynchronous event flow produces an actionable change. Keep the framework if it helps; use another if it does not. This is N=1 experimentation. Its value comes from helping you debug, independently of whether it is “true.”
Related Concepts
- Pattern Matching - Causal edges as matching rules
- State Machines - Causal graphs with time and discrete states
- Prevention Architecture - Blocking paths before they run
- Cybernetics - Feedback loops as causal circuits
- Question Theory - Asking what causes what
- Computation as Core Language - Causality as a computational operation
- Computation as Physical Process - Compute-current moving through stable patterns
- Digital Daoism - Flow paths and natural tendencies
- Moralizing vs Mechanistic - Diagnosing a causal structure without moral judgment
- 30x30 Pattern - Repetition making causal chains cheaper
- Willpower - The resource cost of resisting an active process
- Activation Energy - The cost of a state transition
- Discretization - Dividing flow into manageable segments
Key Principle
An event handler, race condition, missing cleanup step, or canceled routine becomes easier to inspect once you trace what was supposed to cause what. Compare that chain with what occurred, then repair the missing connection or remove the competing one. Arrange the desired sequence to run by default, and judge the model by whether it helps you make that repair.