A live-data workflow built without a single custom operator. Two parallel
HTTPGetOp calls hit Yahoo Finance for a quote and a news headline.
JSONExtractOp pulls fields out of the responses. AIParseNumberOp
coerces stringly-typed numerics to float64. SubFloatOp computes
the price change. AIScoreOp turns the headline into a sentiment score. A
chain of StringConcatOp assembles the final analysis prompt, and
AIComputeStringToStringOp emits the Buy/Hold/Sell recommendation.
What this example teaches
RegisterConst + StringConcatOp chainsHTTPGetOp + JSONExtractOp for two independent fetchesAIParseNumberOp as a fallback when the source returns stringly-typed numericsSubFloatOp) with AI scoring (AIScoreOp) in one graphprovider: "gemini"RegisterConst + StringConcatOpRegisterConst turns a fixed Go value into a
registered op. Combine with the ticker context-val and two StringConcatOp
vertices to build a per-run URL without a custom code.
graph.NewBuilder("stock_analyzer").
Vertex("ticker_input").Op("ticker_const").
Output("Result", "ticker_sym").
// 1a. Build URL
Vertex("build_url").Op("StringFormatOp").
Params(map[string]string{
"template": "https://query2.finance.yahoo.com/v10/finance/quoteSummary/%s?modules=financialData",
}).
Input("Args", "ticker_sym").
Output("Result", "yahoo_url").
// 1b. Fetch Data
Vertex("fetch_data").Op("HTTPGetOp").
Input("URL", "yahoo_url").
Output("Response", "raw_json")
b = graph.new_builder("stock_analyzer")
b.vertex("ticker_input").op("ticker_const") \
.output("Result", "ticker_sym")
# 1a. Build URL
b.vertex("build_url").op("StringFormatOp") \
.params({
"template": "https://query2.finance.yahoo.com/v10/finance/quoteSummary/%s?modules=financialData"
}) \
.input("Args", "ticker_sym") \
.output("Result", "yahoo_url")
# 1b. Fetch Data
b.vertex("fetch_data").op("HTTPGetOp") \
.input("URL", "yahoo_url") \
.output("Response", "raw_json")
const wf = new Workflow("stock_analyzer");
const tickerSym = wf.op.tickerConst();
// 1a. Build URL
const yahooUrl = wf.op.stringFormat({
template: "https://query2.finance.yahoo.com/v10/finance/quoteSummary/%s?modules=financialData",
args: tickerSym,
});
// 1b. Fetch Data
const rawJson = wf.op.httpGet({
url: yahooUrl,
});
Two independent HTTPGetOp vertices. The
engine sees they share no upstream dependency and runs them concurrently. Three
JSONExtractOp vertices then pull the three numeric/string fields the
downstream stages need.
// 2. Analyze with AI
Vertex("extract_metrics").Op("AIExtractJSONOp").
Params(map[string]string{
"operation": "extract currentPrice, targetMeanPrice, and recommendationKey",
"schema": `{"type":"object","properties":{"currentPrice":{"type":"number"},"targetMeanPrice":{"type":"number"},"recommendationKey":{"type":"string"}}}`,
}).
Input("Input", "raw_json").
Output("Result", "metrics")
# 2. Analyze with AI
b.vertex("extract_metrics").op("AIExtractJSONOp") \
.params({
"operation": "extract currentPrice, targetMeanPrice, and recommendationKey",
"schema": '{"type":"object","properties":{"currentPrice":{"type":"number"},"targetMeanPrice":{"type":"number"},"recommendationKey":{"type":"string"}}}',
}) \
.input("Input", "raw_json") \
.output("Result", "metrics")
// 2. Analyze with AI
const metrics = wf.ai.extractJSON({
operation: "extract currentPrice, targetMeanPrice, and recommendationKey",
schema: '{"type":"object","properties":{"currentPrice":{"type":"number"},"targetMeanPrice":{"type":"number"},"recommendationKey":{"type":"string"}}}',
input: rawJson,
});
Once the price floats are parsed, the change is one
SubFloatOp. The headline's sentiment is one AIScoreOp. Two ops,
one a pure function and one a typed AI call, both produce float64 wires the
downstream prompt-assembly stage stringifies and concatenates.
b.Vertex("calc_change").Op("SubFloatOp").
Input("A", "price").Input("B", "prev_close").Output("Result", "change")
b.Vertex("sentiment").Op("AIScoreOp").
Params(map[string]string{
"provider": "gemini",
"model": "gemini-3-flash-preview",
"criterion": "The headline indicates a positive/bullish outlook",
}).
Input("Input", "headline").Output("Result", "sentiment_score")
b.vertex("calc_change").op("SubFloatOp") \
.input("A", "price").input("B", "prev_close").output("Result", "change")
b.vertex("sentiment").op("AIScoreOp") \
.params({
"provider": "gemini",
"model": "gemini-3-flash-preview",
"criterion": "The headline indicates a positive/bullish outlook",
}) \
.input("Input", "headline").output("Result", "sentiment_score")
const change = wf.op.subFloat({
a: price,
b: prevClose,
});
const sentimentScore = wf.ai.score({
provider: "gemini",
model: "gemini-3-flash-preview",
criterion: "The headline indicates a positive/bullish outlook",
input: headline,
});
go run ./examples/stock-analyzer --ticker AAPL
python examples/stock_analyzer/main.py --ticker AAPL
npx tsx examples/stock_analyzer/index.ts --ticker AAPL
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, analyze_stock. 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 quote + news → sentiment → recommendation pipeline with one tool call.
# Speak MCP over stdin/stdout instead of running once
go run ./examples/stock-analyzer --mcp
# Speak MCP over stdin/stdout instead of running once
python examples/stock_analyzer/main.py --mcp
# Speak MCP over stdin/stdout instead of running once
npx tsx examples/stock_analyzer/index.ts --mcp
Register it with any MCP client by pointing the client at that command:
{
"mcpServers": {
"stock-analyzer": {
"command": "go",
"args": ["run", "./examples/stock-analyzer", "--mcp"]
}
}
}