weather
The weather.example microservice is the suite’s canonical answer to “how do I build an agent?” It answers natural-language weather questions — “What should I wear in Paris today?” — by exposing two of its own endpoints to an LLM as tools and letting the model chain them: geocode a location with LatLng, fetch that location’s conditions with Forecast, then compose a conversational reply. It is the Microbus port of the Pydantic AI weather-agent example.
The teaching point is in the shape: the agent is the AskAgent workflow, not a function. Agents in Microbus are workflows, and this is the first example a reader meets when learning to build one, so it leads with that default deliberately. Where chatbox shows the synchronous Chat on-ramp and creditflow carries the larger graph-and-subgraph story, weather is the smallest complete agent.
Depends On
llm.core— the orchestrator that runs the tool-calling loop, resolves the configured provider, and dispatches each tool call over the bus.Answerruns itsChatLoopworkflow as a subgraph.- A real LLM provider key. Unlike chatbox — which ships a mock provider — weather needs a real model to do genuine tool-calling. It passes
llmapi.ProviderAnyandllmapi.ModelDefault, so a single key of any brand (claudellm,chatgptllm, orgeminillm) configured inconfig.local.yamlis enough;llm.coreresolves which one serves the request at runtime.
The example takes no foremanapi dependency at all — see A Provider Does Not Run Its Own Graph.
Try It
With the examples app running and a provider key configured, hit the synchronous endpoint in a browser:
http://localhost:8080/weather.example/ask?q=What+should+I+wear+in+Paris+today%3F
The Ask function runs the agent in-process and returns the model’s answer. It is the browser-clickable twin of the AskAgent workflow — the same tool-calling loop, same tools, same system prompt — that gives the guided tour one URL to click. The durable workflow form (AskAgent) is the one to learn from; Ask is the short synchronous shortcut.
LatLng and Forecast return deterministic mock data, not a call to a live weather service. A handful of well-known cities map to their true coordinates; every other place is hashed to a stable pseudo-coordinate, and forecasts are derived from the coordinate (warmer near the equator, plus a hash-driven wobble). This keeps the example self-contained — the only external dependency is the LLM provider key. Swapping the mock bodies for real HTTP egress calls would not change the agent’s shape.
The Agent Is a Workflow
The distinction the example is built to teach:
- Synchronous
Chat(as in chatbox) runs the whole tool-calling loop inside a single Go call. It persists no state and must finish within one time budget — right for a quick request/response agent. ChatLoopas a workflow (here) is durable: each step persists its state and gets its own time budget, so the agent can span far more work than one request could, survive a worker restart mid-run, and be observed step-by-step in agentstudio.
This demo agent is short enough that it does not strictly need to be a workflow — that Ask can run it synchronously at all is the tell. It is still modeled as one because that is the shape you want the moment the agent grows a validation-retry loop, an approval pause, or more tools — work that outlives a single request budget and can no longer be a synchronous call. The durable form is the teaching surface; keep Ask as the synchronous twin the tour clicks.
The Workflow Graph
AskAgent is a single-node graph. Its one task, Answer, runs llm.core’s ChatLoop as a subgraph. The node count is one, but the depth is real: ChatLoop is itself a multi-step workflow — call the model, execute each tool, loop — so this is genuine subgraph composition, not a graph wrapped around a one-shot call. As the agent grows, nodes are added to this same graph; the function form would have to be rewritten.
Its source (ASKAGENT.mmd):
%%{init: {'themeVariables': {'lineColor': '#32a7c1', 'edgeLabelBackground': '#e5f4f3', 'textColor': '#434343', 'titleColor': '#32a7c1', 'clusterTextColor': '#32a7c1'}}}%%
graph LR
classDef task fill:#32a7c1,color:#f4f2ef,stroke:#32a7c1
classDef term fill:#e5f4f3,color:#434343,stroke:#32a7c1
_title{{"ask-agent"}}:::term --> _start
_start(( )):::term --> t0
t0["Answer"]:::task
t0 --> _end(( )):::termfunc (svc *Service) AskAgent(ctx context.Context) (graph *workflow.Graph, err error) {
graph = workflow.NewGraph("AskAgent")
graph.SetEndpoint("Answer", weatherapi.Answer.URL())
graph.AddTransition("Answer", workflow.END)
return graph, nil
}Code Walkthrough
Source: exampleservices/weather/service.go.
LatLng and Forecast — the Tools
LatLng and Forecast are ordinary functional endpoints. They become tools purely by being named in the tools list passed to the LLM — there is no separate tool-registration step. llm.core resolves each endpoint URL to a JSON Schema by fetching the host’s OpenAPI document, so the field-level jsonschema_description tags on LatLngIn/ForecastOut are what the model reads to decide how to call them.
func (svc *Service) LatLng(ctx context.Context, location string) (lat float64, lng float64, err error) {
key := strings.ToLower(strings.TrimSpace(location))
// Trim a trailing country/region qualifier, e.g. "London, UK" -> "london".
if comma := strings.IndexByte(key, ','); comma >= 0 {
key = strings.TrimSpace(key[:comma])
}
if c, ok := knownCities[key]; ok {
return c.lat, c.lng, nil
}
// Any other place geocodes to a deterministic pseudo-coordinate, keeping the demo self-contained.
h := fnv.New64a()
h.Write([]byte(key))
sum := h.Sum64()
lat = math.Round((float64(sum%13000)/100.0-60.0)*100) / 100 // -60.00 .. 70.00
lng = math.Round((float64((sum>>20)%36000)/100.0-180.0)*100) / 100
return lat, lng, nil
}Forecast derives its summary, temperature, and precipitation chance from the coordinate the same deterministic way. Both return fictional-but-stable numbers; a variant that called a real API through the egress proxy would swap the mock bodies without touching the agent.
Answer — the Workflow Task
Answer is the graph’s one node. It builds the conversation and tool list, then runs ChatLoop as a durable subgraph via llmapi.NewSubgraph(flow):
func (svc *Service) Answer(ctx context.Context, flow *workflow.Flow, question string) (answer string, err error) {
items, tools := weatherAgentPrompt(question)
// ChatLoop is llm.core's multi-step chat workflow, run here as a durable subgraph.
result, _, yield, err := llmapi.NewSubgraph(flow).ChatLoop(ctx, llmapi.ProviderAny, llmapi.ModelDefault, items, tools, nil)
if err != nil {
return "", errors.Trace(err)
}
if yield {
return "", nil // parked, child workflow running
}
return finalAnswer(result)
}The conversation is an ordered []llmapi.Item — a system message and the user’s question — and the tools are the two endpoint URLs:
func weatherAgentPrompt(question string) (items []llmapi.Item, tools []string) {
items = []llmapi.Item{
llmapi.NewMessage("system", "You are a helpful weather assistant. Use the lat-lng tool to geocode a location, then the forecast tool to get its conditions, and answer the question conversationally.").AsItem(),
llmapi.NewMessage("user", question).AsItem(),
}
tools = []string{
weatherapi.LatLng.URL(),
weatherapi.Forecast.URL(),
}
return items, tools
}ChatLoop returns a yield signal when it parks to run its own steps; Answer propagates that by returning cleanly, and the Foreman re-enters the task when the subgraph completes. On completion, finalAnswer pulls the content of the last assistant message out of the returned conversation.
Ask — the Synchronous Twin
Ask runs the identical loop in-process with a single llm.core Chat call rather than as a workflow. It shares the weatherAgentPrompt and finalAnswer helpers with Answer, so the two forms of the agent stay in lockstep:
func (svc *Service) Ask(ctx context.Context, q string) (answer string, err error) {
items, tools := weatherAgentPrompt(q)
result, _, _, err := llmapi.NewClient(svc).Chat(ctx, llmapi.ProviderAny, llmapi.ModelDefault, items, tools, nil)
if err != nil {
return "", errors.Trace(err)
}
return finalAnswer(result)
}Chat and ChatLoop take the same arguments — provider, model, conversation, tools, options — because they are the synchronous and durable forms of the same operation. Both pass llmapi.ProviderAny and llmapi.ModelDefault, so the agent runs against whichever single provider the operator has keyed.
A Provider Does Not Run Its Own Graph
Ask does not go through the Foreman, and this microservice imports no foremanapi at all. A microservice that provides a workflow must not depend on the execution engine to run it: the Foreman is to workflows what the ingress proxy is to inbound HTTP, and a provider no more calls the Foreman to run its own graph than it calls the ingress proxy to serve its own endpoint.
Launching AskAgent durably is the job of the code that owns the triggering event — a UI or API request, a cron ticker, an inbound event — and that code takes the foremanapi.Run dependency, usually in a different microservice. Weather owns no such trigger, so it stays a clean provider with zero foremanapi imports. Ask is the escape hatch that lets the tour click the agent without standing up a separate launcher.
See Also
- LLM integration — the
ChatandChatLoopAPI, provider-agnostic resolution, and the[]Itemconversation model. - LLM tooling — how a plain endpoint becomes a tool the model can call, and the authorization boundary.
- Chatbox example — the synchronous
Chaton-ramp with a mock provider. - Credit flow example — the larger workflow story: fan-out/fan-in, subgraphs, and goto loops.
- Bank support example — the next agent to learn: actor-scoped tools and typed structured output on top of this single-node shape.
- Flight booking example — the agent that parks on a real human-in-the-loop
Interrupt/Resumedecision. - Building agentic workflows — the step-by-step workflow tutorial.
coreservices/llm— the orchestrator’s package reference and configuration.