Examples Stock Analyzer — zero custom ops
Library only

Stock Analyzer

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

  • Zero custom operators — the whole workflow is library ops only
  • Dynamic URL building via RegisterConst + StringConcatOp chains
  • Parallel HTTPGetOp + JSONExtractOp for two independent fetches
  • AIParseNumberOp as a fallback when the source returns stringly-typed numerics
  • Mixing deterministic math (SubFloatOp) with AI scoring (AIScoreOp) in one graph
  • Per-vertex provider selection — every AI op pins provider: "gemini"
View source on GitHub
The workflow

Two parallel branches, one synthesis

det op

Deterministic steps

RegisterConst (×11)

URL prefixes/suffixes, JSON paths, and prompt fragments registered as constant ops.

StringConcatOp chains

Assemble the quote URL, news URL, and the final analysis prompt by chaining concats.

HTTPGetOp (×2 parallel)

Independent fetches for quote and news. Scheduled concurrently by the engine.

JSONExtractOp (×3)

Dot-path extraction of regularMarketPrice, chartPreviousClose, and the latest news title.

SubFloatOp + Float64ToStringOp

Compute and stringify the price change since previous close — pure arithmetic, zero tokens.

AI op

AI steps

AIParseNumberOp (×2)

Coerce the price strings to float64 — fallback for cases where the JSON value isn't a clean number.

AIScoreOp

Score the headline 0–1 against the criterion "indicates a positive/bullish outlook".

AIComputeStringToStringOp

Synthesises the Buy/Hold/Sell recommendation from the assembled prompt with all numeric inputs filled in.

Implementation

Walkthrough

1. Building URLs with RegisterConst + StringConcatOp

RegisterConst 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.

main.go — Stage 1 (Fetching URLs)
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")
main.py — Stage 1 (Fetching URLs)
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")
index.ts — Stage 1 (Fetching URLs)
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,
});

2. Parallel fetch + JSON extraction

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.

main.go — Stage 2 (LLM Extraction)
    // 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")
main.py — Stage 2 (LLM Extraction)
    # 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")
index.ts — Stage 2 (LLM Extraction)
    // 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,
    });

3. Mixing deterministic math and AI scoring

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.

main.go — buildGraph()
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")
main.py — buildGraph()
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")
index.ts — buildGraph()
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,
});

Run it

shell
go run ./examples/stock-analyzer --ticker AAPL
shell
python examples/stock_analyzer/main.py --ticker AAPL
shell
npx tsx examples/stock_analyzer/index.ts --ticker AAPL

Run it as an MCP server

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.

shell
# Speak MCP over stdin/stdout instead of running once
go run ./examples/stock-analyzer --mcp
shell
# Speak MCP over stdin/stdout instead of running once
python examples/stock_analyzer/main.py --mcp
shell
# 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:

.mcp.json
{
  "mcpServers": {
    "stock-analyzer": {
      "command": "go",
      "args": ["run", "./examples/stock-analyzer", "--mcp"]
    }
  }
}
← Remote MCP Next: RAG (BM25) →