Fetches live weather from wttr.in (or an offline fixture), uses AI to parse raw number strings, classify conditions into multiple labels, then computes a deterministic temperature band and boolean wet/windy flags. All signals are packed into a single description and passed to an AI for an outfit recommendation. An orthogonal probe appends an extreme-weather warning when conditions are unusual.
What this example teaches
IfFloatGtOp compares the parsed float against a constant wire, producing a boolean without AIContextValOp vertices merged by CoalesceNStringOp — exactly one string firesPackOutfitInputsOp) that assembles five input wires into a formatted summary string, decoupling data extraction from prompt constructionAIBoolOp check that appends a warning suffix without blocking the main advice pathAI handles the parts the API returns as opaque strings; deterministic ops compute the flags and bands; a custom packing op bundles everything before the final AI call.
The wttr.in response stores numeric fields as JSON strings. JSONExtractOp pulls them out as Go strings; AIParseNumberOp converts each to a float64. The four extractions share the body wire and run concurrently, as do the three number parsers.
// Stage 2: extract four string fields in parallel.
for _, f := range []struct{ path, wire string }{
{"current_condition.0.temp_C", "temp_str"},
{"current_condition.0.precipMM", "precip_str"},
{"current_condition.0.windspeedKmph", "wind_str"},
{"current_condition.0.weatherDesc.0.value", "desc_str"},
} {
b.Vertex("extract_"+f.wire).Op("JSONExtractOp").
Input("JSON", "body").
Input("Path", f.wire+"_path").
Output("Value", f.wire)
}
// Stage 3: three AIParseNumberOps run concurrently.
b.Vertex("parse_temp").Op("AIParseNumberOp").
Params(map[string]string{"operation": "extract temperature as plain number"}).
Input("Input", "temp_str").
Output("Result", "temp_c").
Three ContextValOp vertices each read a band label from context and carry a numeric predicate. The predicates compare temp_c against thresholds. Exactly one fires per execution; CoalesceNStringOp merges the three wires into a single band string. Zero AI tokens for this categorisation.
for _, band := range []struct{ name, cond string }{
{"cold", "temp_is_cold"}, // temp < 10°C
{"mild", "temp_is_mild"}, // 10 ≤ temp < 22°C
{"hot", "temp_is_hot"}, // temp ≥ 22°C
} {
// Registered: builtin.ContextValFactory[string](coldBandKey{} / mildBandKey{} / hotBandKey{})
// Injected: ctx = context.WithValue(ctx, coldBandKey{}, "cold") etc.
b.Vertex(band.name+"_const").Op(band.name+"_const").
Condition(band.cond).
ConditionInput("temp_c").
Output("Result", band.name+"_band")
}
b.Vertex("band_merge").Op("CoalesceNStringOp").
Params(map[string]int{"n": 3}).
Merge(config.MergeCoalesce).
Input("Input0", "cold_band").
Input("Input1", "mild_band").
Input("Input2", "hot_band").
Output("Result", "band")
IfFloatGtOp compares two float64 wires and emits a boolean. The thresholds (0.1 mm precip, 25 km/h wind) are constant wires — they can be changed by editing the graph, not the library code.
b.Vertex("precip_thresh").Op("ConstFloat64Op").
Params(map[string]string{"Value": "0.1"}).
Output("Result", "precip_thresh")
b.Vertex("wet_check").Op("IfFloatGtOp").
Input("A", "precip_mm").
Input("B", "precip_thresh").
Output("Match", "wet")
b.Vertex("wind_thresh").Op("ConstFloat64Op").
Params(map[string]string{"Value": "25.0"}).
Output("Result", "wind_thresh")
b.Vertex("windy_check").Op("IfFloatGtOp").
Input("A", "wind_kph").
Input("B", "wind_thresh").
Output("Match", "windy")
The unusual-weather probe reads from desc_str — the same raw description string used earlier — without depending on any of the downstream signal wires. This means it can run in parallel with the number parsers. Its boolean output feeds a SelectStringOp that chooses between a warning string and an empty string; StringConcatOp appends the result to the outfit advice.
// Runs in parallel with parse_temp/parse_precip/parse_wind and classify.
b.Vertex("unusual_check").Op("AIBoolOp").
Params(map[string]string{"predicate": "Is the described weather unusual or extreme?"}).
Input("Input", "desc_str").
Output("Result", "unusual_flag")
b.Vertex("warning_select").Op("SelectStringOp").
Input("Cond", "unusual_flag").
Input("IfTrue", "warning_str"). // " ⚠ unusual weather"
Input("IfFalse", "empty_str").
Output("Result", "warning_suffix")
b.Vertex("final_concat").Op("StringConcatOp").
Input("A", "outfit_advice").
Input("B", "warning_suffix").
Output("Result", "final_advice")
Three fixtures cover all three temperature bands — London (mild), Reykjavik (cold, likely unusual), Singapore (hot).
export CLAUDE_API_KEY=<your key>
# Live API
go run ./examples/weather-advisor --city London
go run ./examples/weather-advisor --city Reykjavik
go run ./examples/weather-advisor --city Singapore
# Offline fixtures
go run ./examples/weather-advisor --fixture examples/weather-advisor/testdata/weather/london.json
go run ./examples/weather-advisor --fixture examples/weather-advisor/testdata/weather/reykjavik.json
The same binary is dual-mode. Pass --mcp and instead of running once and exiting it speaks the Model Context Protocol over stdin/stdout, exposing the entire workflow as a single MCP tool, advise_outfit. The Go SDK derives the tool's input schema from the workflow's UserInput struct and validates every tools/call request against it, so an agent runs the whole fetch → bands/flags → classify → recommend pipeline with one tool call.
# Speak MCP over stdin/stdout instead of running once
go run ./examples/weather-advisor --mcp
Register it with any MCP client by pointing the client at that command:
{
"mcpServers": {
"weather-advisor": {
"command": "go",
"args": ["run", "./examples/weather-advisor", "--mcp"]
}
}
}
Measured on testdata/weather/london.json. The weather payload is tiny, making this the lowest-cost example. Four AIComputeOp calls cover the three number parsers and the final outfit writer.
| Op | Calls | Input tokens | Output tokens | Total tokens |
|---|---|---|---|---|
| AIBoolOp | 1 | 61 | 4 | 65 |
| AIClassifyMultiLabelOp | 1 | 84 | 4 | 88 |
| AIComputeOp | 4 | 303 | 77 | 380 |
| Total | 6 | 448 | 85 | 533 |