Walker Response Patterns#
Walkers traverse a graph, visiting nodes and executing logic at each step. But how do you get data out of a walker after it finishes? That's what the report statement is for -- it's the primary mechanism for walkers to communicate results back to the code that spawned them.
This reference covers the report mechanism and the common patterns for structuring walker responses. Choosing the right pattern matters because it affects how your client code (whether a with entry block, an API endpoint, or another walker) consumes the results.
Related:
- Graph Operations - Node creation, traversal, and deletion
- Part III: OSP - Walker and node fundamentals
- Build an AI Day Planner - Full tutorial using these patterns
The .reports Array#
Every time a walker executes a report statement, the value is appended to a .reports array on the response object. When you spawn a walker with root spawn MyWalker(), the returned object contains this array, giving you access to everything the walker reported during its traversal. Think of report as the walker's "return channel" -- except that a walker can report multiple times as it moves through the graph, accumulating results along the way.
Note
The report statement also prints each reported value to stdout as a side effect. This means you will see the reported values printed to the console in addition to them being collected in .reports.
walker:priv MyWalker {
can do_work with Root entry {
report "first"; # reports[0]
report "second"; # reports[1]
}
}
with entry {
# Spawning the walker
response = root spawn MyWalker();
print(response.reports); # ["first", "second"]
}
Common Patterns#
Pattern 1: Single Report (Recommended)#
The cleanest pattern accumulates data internally and reports once at the end:
node Item {
has data: str;
}
walker:priv ListItems {
has items: list = [];
can collect with Root entry {
visit [-->];
}
can gather with Item entry {
self.items.append(here.data);
}
can finish with Root exit {
report self.items; # Single report with all data
}
}
with entry {
# Usage
result = root spawn ListItems();
items = result.reports[0]; # The complete list
}
When to use: Most read operations, listing data, aggregations.
This is the accumulator pattern -- the standard approach for collecting data from a graph traversal. The walker flows through three stages:
- Enter root → initiate traversal with
visit [-->] - Visit each node → gather data into walker state (
self.items) - Exit root → report the accumulated result
The with Root exit ability fires after the walker has finished visiting all queued nodes and returns to root, making it the ideal place for a single consolidated report.
Accumulator in Frontend
When calling this pattern from client code, access the result with result.reports[0] -- there is always exactly one report containing the full collection.
For a complete walkthrough of this pattern in a full-stack app, see Build an AI Day Planner.
Pattern 2: Report Per Node#
Reports each item as it's found during traversal:
node Item {
has name: str;
}
walker:priv FindMatches {
has search_term: str;
can search with Root entry {
visit [-->];
}
can check with Item entry {
if self.search_term in here.name {
report here; # One report per match
}
}
}
with entry {
# Usage
result = root spawn FindMatches(search_term="test");
matches = result.reports; # Array of all matching nodes
}
When to use: Search operations, filtering, finding specific nodes.
Pattern 3: Operation + Result#
Performs an operation and reports a summary:
node Item {
has name: str;
}
walker:priv CreateItem {
has name: str;
can create with Root entry {
new_item = here ++> Item(name=self.name);
report new_item[0]; # Report the created item
}
}
with entry {
# Usage
result = root spawn CreateItem(name="New Item");
created = result.reports[0]; # The new item
}
When to use: Create, update, delete operations.
Pattern 4: Nested Walker Spawning#
When one walker spawns another, use has attributes to pass data between them instead of relying on reports:
walker:priv InnerWalker {
has result: str = "";
can work with Root entry {
self.result = "inner data";
}
}
walker:priv OuterWalker {
can work with Root entry {
# Spawn inner walker
inner = InnerWalker();
root spawn inner;
# Access inner walker's data via its attributes
report {"outer": "data", "inner": inner.result};
}
}
with entry {
# Usage
result = root spawn OuterWalker();
# result.reports[0] = {"outer": "data", "inner": "inner data"}
}
Important: When spawning walkers from within other walkers, the inner walker's reports list may not be accessible from the parent context. Use has attributes on the inner walker to communicate results back to the outer walker.
Pattern 5: Multiple Reports (Complex Operations)#
Some operations naturally produce multiple reports:
def do_processing(input: str) -> list {
return [input, input + "_processed"];
}
walker:priv ProcessAndSummarize {
has input: str;
can process with Root entry {
# First report: raw results
results = do_processing(self.input);
report results;
# Second report: summary
report {
"count": len(results),
"status": "complete"
};
}
}
with entry {
# Usage
result = root spawn ProcessAndSummarize(input="data");
raw_results = result.reports[0]; # First report
summary = result.reports[1]; # Second report
}
When to use: Operations that produce both detailed results and summaries.
Safe Access Patterns#
Always handle the possibility of empty reports:
walker:priv MyWalker {
can work with Root entry {
report "data";
}
}
def process(item: any) {
print(item);
}
with entry {
# Safe single report access
result = root spawn MyWalker();
data = result.reports[0] if result.reports else None;
# Safe with default value
data = result.reports[0] if result.reports else [];
# Check length for multiple reports
if result.reports and len(result.reports) > 1 {
first = result.reports[0];
second = result.reports[1];
}
# Iterate all reports
for item in (result.reports if result.reports else []) {
process(item);
}
}
Response Object Structure#
The full response object from root spawn Walker():
walker:priv MyWalker {
can work with Root entry {
report "result";
}
}
with entry {
response = root spawn MyWalker();
# Available properties
print(response.reports); # Array of all reported values
}
Best Practices#
- Prefer single reports - Accumulate data and report once at the end
- Use
with Root exit- Best place for final reports after traversal - Document report structure - Comment what each report index contains
- Always check
.reports- It may be empty or undefined - Keep reports serializable - Stick to dicts, lists, strings, numbers, bools
Anti-Patterns#
Don't: Report in a loop without accumulation#
node Item {
has data: str;
}
# Bad: Creates many small reports
walker:priv BadPattern {
can process with Item entry {
report here.data; # N reports for N items
}
}
# Good: Accumulate and report once
walker:priv GoodPattern {
has items: list = [];
can start with Root entry {
visit [-->];
}
can process with Item entry {
self.items.append(here.data);
}
can finish with Root exit {
report self.items; # One report with all items
}
}
Don't: Assume report order without documentation#
walker:priv MyWalker {
can work with Root entry {
report ["item1", "item2"];
report {"count": 2};
}
}
with entry {
result = root spawn MyWalker();
# Bad: Magic indices
data = result.reports[0];
meta = result.reports[1];
# Good: Document or structure clearly
# reports[0]: List of items
# reports[1]: Metadata object
data = result.reports[0] if result.reports else [];
meta = result.reports[1] if len(result.reports) > 1 else {};
}