Skip to main content

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

  1. Write a minimal .bloge file that the compiler accepts.
  2. Load and execute that file from Java.
  3. Build the same graph using the fluent Java API.
  4. Understand what ctx, input, output, and timeout mean at runtime.

Prerequisites

Source Examples

FileWhat it shows
ch02/hello-world.blogeThe absolute minimum: one graph, one node, one input binding
ch02/two-node-chain.blogeTwo nodes with an explicit dependency
input-bindings.blogeConformance 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.

  1. 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
  2. Run the companion example tests:

    mvn -f head-first-bloge-examples/pom.xml test
  3. 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

Diagram: 02-your-first-graph figure 1

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

Diagram: 02-your-first-graph figure 2

  1. You create a GraphContext and put values in it.
  2. The engine resolves input bindings — expressions like ctx.message — to pull data from the context (or from upstream node outputs).
  3. Each node's operator receives the assembled input, does its work, and returns an output.
  4. 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:

TokenMeaning
graph helloWorldDeclares a graph named helloWorld
node echoDeclares a node named echo
: EchoOperatorBinds 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 = 1sThe 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.

Diagram: one success and two failure stages

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

StageObservable resultRepair the smallest owner
CompileA node has no Operator binding; diagnostic points at the declarationFix the .bloge node declaration
LoadMissingOperator is not in the registryRegister the capability or correct its name
ExecuteA bound Operator returns or throwsInspect 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:

ConceptWhat it means
depends_on = [normalizeName]Explicit dependency — buildGreeting waits for normalizeName
normalizeName.output.valueRead 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 needConfiguration owner and later chapter
Reject an incompatible input/output shapeGraph.builder().schemaValidation(...) and .contracts(...) — Chapter 8
Observe or wrap each invocationGraphEngine.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:

  1. Add a second node called shout that converts the echo output to uppercase. Bind its input to echo.output.message.
  2. Give it a timeout of 2s.
  3. 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

  1. What are the four things you need to execute a graph? (A Graph, an OperatorRegistry, a GraphContext, and a GraphEngine.)
  2. If a node's input binding references ctx.userId, where does that value come from? (From the GraphContext you create before execution.)
  3. Can you mix literal values and upstream references in the same input block? (Yes — see the conformance fixture where x = ctx.foo and y = "bar" coexist.)
  4. 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.)
  5. Design question: If a graph has ten nodes but only two of them need data from the context, does every node need an input block? (No — only nodes that consume external data or upstream outputs need input. A node with no input block receives an empty input map.)

Lab

  1. Create a new file my-first-graph.bloge with a graph greet containing:
    • A fetchName node that reads ctx.name.
    • A buildGreeting node that reads fetchName.output.name and produces a greeting string.
  2. In Java, register both operators, load the file, execute the graph with ctx.put("name", "Alice"), and print the result.
  3. Verify that buildGreeting ran after fetchName by checking the GraphResult.

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 graph block, at least one node with an operator binding, and an input block.
  • GraphContext carries the initial data; input bindings wire it into nodes.
  • The fluent Java API and the .bloge DSL produce the same runtime graph.
  • Dependencies can be explicit (depends_on) or inferred from .output references.

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.


Coding Agent: Open the versioned task guide.