# The canonical example flows — data only, like sdks.yaml beside it. # # Every Hanzo SDK ships the SAME six examples under `examples//`, in this # order, under these names. A reader who knows one language's set can then find # their way around another's without re-learning what the repo chose to # demonstrate. This file is what makes "the same six" a fact rather than six # repos independently remembering to agree. # # `operations` are operationIds in hanzo.yaml, in call order. That is the part a # language cannot fudge: test_flows.py asserts every id here exists in the # published document, so a route that stops being served breaks THIS gate — one # place — instead of breaking six SDKs quietly and separately. Since hanzo.yaml # is now derived from hanzo-inc/cloud's own emission, an id here is a served # operation by construction, and there is no way to name one that is not. # # Existing in the spec is necessary and not sufficient: an id can resolve and # still 404 on the wire. Every flow below was PROBED at api.hanzo.ai, and where # the obvious route did not answer, `notes` says what answered instead and how # the dead one failed. # # THE TEST, in two halves, because each half alone has been wrong here once: # # • 403 PROVES THE ROUTE EXISTS. Auth middleware runs before the handler, so a # mounted route refuses (401/403) and an unmounted one is 404/405. That is # the only reliable liveness signal — a 404 BODY proves nothing, since a # live handler says "not found" too. # • PROBE THE METHOD THE DOCUMENT DECLARES, not GET. A POST-only route also # 404s on GET: POST /v1/mcp answers 200 with 796 tools while GET /v1/mcp is # 404, and reading that 404 as absence is how the fleet's one MCP endpoint # stayed undeclared. One verb is never a liveness probe. # # And a 200 is not proof either — see `hello`: /v1/ai/account answers 200 with # type="anonymous-user" to a request carrying no key at all. # # Nothing here is executable and no SDK repo duplicates it. What each example # prints is the language's business; WHICH routes it calls is not. # # Rules an example must hold to, in every language: # • read the key from HANZO_API_KEY; # • default to https://api.hanzo.ai, overridable by HANZO_BASE_URL; # • resolve both in ONE shared place per repo, not once per flow; # • be compiled or imported by that repo's CI gate, so it cannot rot. flows: hello: summary: Identity — prove the key works and print who it belongs to. # The route must FAIL on a bad key, which is the whole point of the flow, so # this choice is made by probing api.hanzo.ai rather than by reading the # document. Three candidates were disqualified for answering 200 to a caller # with no credential at all — GET /v1/ai/account (type="anonymous-user"), # GET /v1/iam/whoami and GET /v1/iam/account (both {"status":"error", # "msg":"please sign in first"}, 200) — and a `hello` built on any of them # prints a cheerful identity for a key that would 403 everywhere else. # # get_models comes FIRST and needs no key: the document now declares a # `bearer` scheme, applies it to every operation, and exempts exactly four # with `security: []` — get_models, get_models_providers, get_commands, # get_openapi.json. So "which routes are open" is a fact an SDK can read # rather than a probe result recorded in this file, and hello can open with # a call that SUCCEEDS for a reader who has no credential at all. It is a # reachability line, not the identity proof; get_account_keys is still that. operations: [get_models, get_account_keys] notes: >- The keys route answers under the app that owns it — /v1/account/keys — and the bare /v1/keys this file used to name is a 404 today. Re-probed with the nonsense-sibling control the moment it moved: GET /v1/account/keys answers 403 {"code":"forbidden","detail":"sign in to manage API keys"} with NO key and with a bogus one, while GET /v1/keys-zzq9 answers 404 — so the 403 is this route refusing, not a wildcard relay. With a real key it returns the caller's own keys. An SDK's hello must be the call that says no. Both halves are what makes the flow readable: an unreachable gateway and a rejected credential both end in "no", and only the open call separates them. This replaces bot_authMe (GET /v1/bot/auth/me), which no longer resolves: cloud relays all of /v1/bot through one `app.All("/v1/bot/*")`, so the document has `/v1/bot/{wildcard1}` and no operation named that address. The old id existed only in bot/openapi.yaml, and a hand-authored id vanishes with the hand-authored spec — which is exactly why an id here must come from the served document. chat: summary: One completion. operations: [post_chat_completions] notes: >- POST /v1/chat/completions, the gateway's own inference route. The id lost its `v1` when the default version left the operationId; the address did not move. The document states that address and NOT its shape — no requestBody and no responses — so a generated method takes no prompt and hands back an opaque body. No language can ship this as an example that runs, and python, typescript and go each ship five. The flow keeps its place here because the operation is served: the day cloud's handler declares its In and Out types, the sixth example returns everywhere at once with nothing left to decide. Non-streaming when it does. Streaming is a different transport (SSE) that the generated clients return as an opaque body, so demonstrating it here would teach the wrong shape. Print choices[0].message.content. money: summary: Balance, then the usage that moved it. operations: [get_billing_balance, get_billing_usage] notes: >- Neither takes an org: both derive the tenant server-side from the JWT `owner` claim, so a key can only read its own money. Both ids are cloud's cloud's, and so is the prose: it comes out of the handler's doc comment. /v1/billing/usage takes `start` and `end`, and both operations carry a typed 2xx body, so an SDK reads a struct rather than decoding by hand. Those are cloud's own declarations, from the handlers' `In`/`Out` types — not a shape anybody wrote here on their behalf. store: summary: KV round-trip — create a store, read it back, delete it. operations: [post_provisioning_kv, get_provisioning_kv_by_name, delete_provisioning_kv_by_name] notes: >- The delete belongs in the language's finally/defer, so a failed read still cleans up instead of leaving the store for the next run to collide with. This is the provisioning plane — create a KV STORE, read it, drop it — at its served address, /v1/provisioning/kv. This file named /v1/instances/kv before, and that address is GONE: probed at api.hanzo.ai it answers 404 and so does its /v1/instances/kv-zzq9 control, which is this repo's own definition of refuted. The store plane answers under the app that registers it — GET /v1/provisioning/kv answers 403 {"code":"forbidden","detail":"a validated principal is required"} while /v1/provisioning/kv-zzq9 answers 404, the existence proof this file requires (auth runs before the handler, so only a mounted route can refuse). The bare /v1/kv spellings are a SEPARATE value plane keyed by bucket (/v1/kv/{bucket}/{key}); the bare /v1/kv itself answers 200 with a package descriptor to anyone, which is exactly the shape a round-trip must not be built on. agent: summary: Create an agent, run it, read the run back. operations: - post_agents - post_agents_by_ref_run - get_agents_by_ref_runs notes: >- `ref` accepts the public id (agent_...) OR the org-unique name, which is why run and read can both use the name just created without waiting for an id. Names are org-unique, so an example must not hardcode one. The last step is the RUN list, not the agent read: a run is asynchronous, so the example polls it until the run it started is terminal. tools: summary: List the MCP tools this key can reach. operations: [get_tools] notes: >- GET /v1/tools is the catalogue read: every tool the caller's org and project can reach, from every source, each flagged with whether it is activated. Assert the count is greater than zero and print three names. This flow named `mcp_rpc` (POST /v1/mcp, the JSON-RPC endpoint) until the document became the only authority on what exists. That endpoint IS live — POST /v1/mcp answers 200 with the tool list while POST /v1/mcp-zzq9 answers 404, so the address is real and the control refutes nothing — but cloud's emission does not declare it, so it has no operationId and no generated method. An example may not reach for an address the document does not carry: that is a hand-rolled HTTP call inside a generated client, the exact drift these SDKs exist to prevent. `POST /v1/mcp` returns here the release cloud declares it (LLM.md, "Handed to hanzo-inc/cloud"). Note for whoever writes that declaration: JSON-RPC reports failure INSIDE a 200, so a client reads `error` before `result` and `result.isError` before `result.content`. The same endpoint with method=tools/call runs one.