Online edition for BLOGE
0.9.8-RC1· facts verified 2026-09-15 · 中文
Chapter 2 — Your First Graph
This chapter walks through the smallest runnable BLOGE graph, including how it is written, compiled, and executed in both DSL form and the fluent Java API.
Learning Goals
- Write a minimal
.blogefile that the compiler accepts. - Load and execute that file from Java.
- Build the same graph using the fluent Java API.
- Understand what
ctx,input,output, andtimeoutmean at runtime.
Prerequisites
- Chapter 1 — What BLOGE Is and Why It Exists
- JDK 25+ with
--enable-preview, Maven 3.6+ (see Getting Started)
Source Examples
| File | What it shows |
|---|---|
ch02/hello-world.bloge | The absolute minimum: one graph, one node, one input binding |
ch02/two-node-chain.bloge | Two nodes with an explicit dependency |
input-bindings.bloge | Conformance fixture: basic input binding syntax |
Why This Matters
Chapter 1 established BLOGE's definition, core properties, and the limits of hand-written orchestration. This chapter carries those ideas into the smallest runnable example so that graph authoring, loading, and execution can be seen in concrete form.
A one-node graph is already enough to place four runtime concepts on a single execution path:
- How data enters the graph (the context).
- How a node receives data (the input block).
- How the engine invokes your code (the operator).
- How to read the result.
Everything else in this book builds on these four ideas.
Setup
If you want to follow this chapter hands-on, use the companion project that now lives beside the book in this repository.
-
Install the core BLOGE artifacts that the standalone companion module depends on:
mvn -q -pl bloge-core,bloge-dsl,bloge-test -am install -DskipTests -Dspotbugs.skip=true -
Run the companion example tests:
mvn -f head-first-bloge-examples/pom.xml test -
Open the Chapter 2 companion files:
The point is not only to read the snippets in this chapter. Run them, inspect the tests, and keep the book open next to the companion project so the example stays grounded in something executable.
Project layout
Each chapter's .bloge files live in src/main/resources/bloge/chNN/. The
test classes under src/test/java/ compile or execute those files. As you
progress through the book, run the matching chapter examples so your
understanding stays tied to something real.
Mental Model
- You create a GraphContext and put values in it.
- The engine resolves input bindings — expressions like
ctx.message— to pull data from the context (or from upstream node outputs). - Each node's operator receives the assembled input, does its work, and returns an output.
- After execution, you read results from the GraphResult.
First Working Example
The DSL way
This is the content of
ch02/hello-world.bloge:
graph helloWorld {
node echo : EchoOperator {
input {
message = ctx.message
}
timeout = 1s
}
}
That is a complete, valid BLOGE graph. The table below breaks it down:
| Token | Meaning |
|---|---|
graph helloWorld | Declares a graph named helloWorld |
node echo | Declares a node named echo |
: EchoOperator | Binds the node to the operator class EchoOperator |
input { message = ctx.message } | The node's input field message is bound to the context value message |
timeout = 1s | The node must complete within 1 second |
To load and run this from Java:
// 1. Register your operator
OperatorRegistry registry = new DefaultOperatorRegistry();
registry.register("EchoOperator", (input, opCtx) -> input);
// 2. Load the DSL file
GraphLoader loader = new GraphLoader(registry);
Graph graph = loader.load(Path.of("src/main/resources/bloge/hello-world.bloge"));
// 3. Build the context
GraphContext ctx = new GraphContext();
ctx.put("message", "hello");
// 4. Execute
GraphEngine engine = GraphEngine.builder().registry(registry).build();
GraphResult result = engine.execute(graph, ctx);
The Java API way
The same graph, built with the fluent Java API:
Graph graph = Graph.builder("helloWorld")
.node("echo", (Map<String, Object> input, OperatorContext opCtx) ->
Map.of("message", opCtx.graphContext().get("message", String.class)))
.timeout(Duration.ofSeconds(1))
.build();
GraphContext ctx = new GraphContext();
ctx.put("message", "hello");
GraphEngine engine = GraphEngine.builder().build();
GraphResult result = engine.execute(graph, ctx);
Both approaches produce the same runtime graph. The DSL is declarative and external; the Java API is type-safe and in-code. Most teams start with DSL for prototyping and move stable operator logic into Java classes.
Read Three Outcomes Before Adding Configuration
A first graph is not complete when it merely compiles. You should be able to tell, from a small result, whether execution succeeded and where two common failures stopped. That gives every later option a purpose instead of turning the Builder API into a catalogue to memorise.
Outcome 1 — a value came back
The RC1 foundation probe runs hello-world with message=hello and checks the
result rather than relying on a lack of exceptions:
graph: hello-world status: SUCCESS
node: echo status: EXECUTED
output.message: hello
Three facts are separate: the graph completed, echo executed, and its output
has the expected value. Keep all three when the graph grows.
Outcomes 2 and 3 — execution never began
| Stage | Observable result | Repair the smallest owner |
|---|---|---|
| Compile | A node has no Operator binding; diagnostic points at the declaration | Fix the .bloge node declaration |
| Load | MissingOperator is not in the registry | Register the capability or correct its name |
| Execute | A bound Operator returns or throws | Inspect node status, output, and failure evidence |
Compile and load failures should show operator calls = 0. Retrying the graph
cannot repair either one because the business code was never dispatched.
Only now do Builder options become useful: listeners observe these stages, interceptors wrap actual invocations, concurrency changes scheduling, and a checkpoint store changes retained execution state. Start from an observed problem, then select the option that owns it.
Break It Apart
Adding a second node
Here is
ch02/two-node-chain.bloge:
graph twoNodeChain {
node normalizeName : NormalizeNameOperator {
input { name = ctx.name }
timeout = 1s
}
node buildGreeting : BuildGreetingOperator {
depends_on = [normalizeName]
input { value = normalizeName.output.value }
timeout = 1s
}
}
New concepts:
| Concept | What it means |
|---|---|
depends_on = [normalizeName] | Explicit dependency — buildGreeting waits for normalizeName |
normalizeName.output.value | Read the value field from the upstream node's output |
Note that even without depends_on, the engine would infer the dependency from
the normalizeName.output.value reference. The explicit declaration is optional
but makes intent clear.
Conformance baseline
The conformance suite includes a minimal input-binding fixture
(input-bindings.bloge):
graph g {
node a : Op {
input {
x = ctx.foo
y = "bar"
}
}
}
This shows that input bindings can be context references (ctx.foo) or
literal values ("bar").
Add configuration only when an observed problem owns it
Do not copy every Builder option into the first graph. Keep this routing table and return when the story reaches the corresponding problem:
| Observed need | Configuration owner and later chapter |
|---|---|
| Reject an incompatible input/output shape | Graph.builder().schemaValidation(...) and .contracts(...) — Chapter 8 |
| Observe or wrap each invocation | GraphEngine.builder().listeners(...) and .interceptors(...) — Chapter 19 |
| Bound simultaneous work | .maxGlobalConcurrency(...) — Chapter 21 |
| Retain or migrate execution state | .executionCheckpointStore(...), .checkpointCodec(...), .versionMismatchPolicy(...) — Chapter 13 |
| Resolve tenant scope and admission | .tenantContextResolver(...), .tenantResourcePolicy(...) — Chapter 21 |
| Replace scheduling time | .schedulerTimerSupport(...) — Chapters 11–12 |
Graph.builder() describes one graph; GraphEngine.builder() configures the
runtime that can execute many graphs. If you cannot name the observed problem,
keep the default.
Common Trap
❌ Forgetting the operator binding
// WRONG — the compiler requires an operator type
node echo {
input { message = ctx.message }
}
Every node must declare an operator with the : OperatorName syntax. Without
it, the compiler does not know which code to run.
// CORRECT
node echo : EchoOperator {
input { message = ctx.message }
}
What Goes Wrong
Missing operator binding — the compiler error
If you forget the operator type:
node echo {
input { message = ctx.message }
}
the parser stops immediately. The failure message comes from the grammar itself:
GraphDefinitionException: Expected ':' after node id
If you keep the colon but omit the operator name, the parser follows up with
Expected operator reference.
Fix: Always declare the operator with : OperatorName.
Unregistered operator — the load-time error
If the DSL names an operator that does not exist in the registry:
registry.register("EchoOperator", (input, opCtx) -> input);
Graph graph = loader.load(Path.of("hello-world.bloge"));
// But the DSL references "ShoutOperator" which was never registered
loading fails before execution begins:
GraphDefinitionException: Node 'shout' references unregistered operator 'ShoutOperator'
Fix: Register every operator name that appears in your .bloge file before loading or executing the graph.
Guided Rewrite
Start from the hello-world graph and extend it:
- Add a second node called
shoutthat converts the echo output to uppercase. Bind its input toecho.output.message. - Give it a timeout of
2s. - Predict the execution order. Which node runs first? Why?
Your DSL should look something like:
graph helloWorld {
node echo : EchoOperator {
input { message = ctx.message }
timeout = 1s
}
node shout : ShoutOperator {
input { message = echo.output.message }
timeout = 2s
}
}
Because shout references echo.output, the engine infers the dependency and
runs echo first.
Brain Check
- What are the four things you need to execute a graph? (A Graph, an OperatorRegistry, a GraphContext, and a GraphEngine.)
- If a node's input binding references
ctx.userId, where does that value come from? (From the GraphContext you create before execution.) - Can you mix literal values and upstream references in the same input block?
(Yes — see the conformance fixture where
x = ctx.fooandy = "bar"coexist.) - You are sketching a workflow in a workshop and want the fastest edit-run feedback loop. Would you start with the DSL file or the fluent Java API, and what would make you switch later? (Most teams start with DSL because the graph shape is easier to see and edit. They switch to Java API when they need stronger in-code reuse, tighter typing, or deeper application-level composition.)
- Design question: If a graph has ten nodes but only two of them need data
from the context, does every node need an
inputblock? (No — only nodes that consume external data or upstream outputs needinput. A node with no input block receives an empty input map.)
Lab
- Create a new file
my-first-graph.blogewith agraph greetcontaining:- A
fetchNamenode that readsctx.name. - A
buildGreetingnode that readsfetchName.output.nameand produces a greeting string.
- A
- In Java, register both operators, load the file, execute the graph with
ctx.put("name", "Alice"), and print the result. - Verify that
buildGreetingran afterfetchNameby checking theGraphResult.
Refer to Getting Started for project setup details.
Experiment acceptance card
- Expected and observed: The minimal graph returns SUCCESS, marks nodes EXECUTED, and exposes the final output.
- Failure and recovery: Misspell one operator name; restore it after the compile/load error and rerun.
- Proof boundary: Proves minimal wiring and result reading, not retries, durability, or business correctness.
- Exercise contract: Hello graph; change one operator identifier; deliver success and failure receipts; stop when both repeat.
Recap
- A minimal BLOGE graph needs a
graphblock, at least onenodewith an operator binding, and aninputblock. - GraphContext carries the initial data; input bindings wire it into nodes.
- The fluent Java API and the
.blogeDSL produce the same runtime graph. - Dependencies can be explicit (
depends_on) or inferred from.outputreferences.
Next Step
Chapter 3 — Thinking in Dependencies extends the discussion into dependency-based scheduling and why good graph design starts from dependencies rather than line-by-line sequence.
Reference Links
- Getting Started — full setup guide
- DSL Specification — complete grammar
ch02/hello-world.bloge— companion sourcech02/two-node-chain.bloge— companion sourcehead-first-bloge-examples/README.md— companion project usageinput-bindings.bloge— conformance fixture
Coding Agent: Open the versioned task guide.