---
title: "Go SDK"
description: "The typed Go client of the runtime API, a module of its own, with typed agents per tenant."
canonical_url: "https://docs.oz0.ai/build/apps/sdk-go"
---
# Go SDK

> The typed Go client of the runtime API, a module of its own, with typed agents per tenant.

For: application developers.

Use package `oz0` from module `go.oz0.ai/oz0/sdk/go` to start agents and flows, wait for answers, watch jobs, answer questions, decide approvals, read the catalog and inspect cost.

The SDK depends on the Connect client in its `gen` directory, connect-go, protobuf-go and the standard library. It does not depend on the server.

<callout icon="i-ph-info">

The module is not published yet: until the first public release, point a `replace` directive at `sdk/go` in a checkout of the repository. Its tags will be `sdk/go/vX.Y.Z`.

</callout>

## Connect

```go [main.go]
c, err := oz0.New("https://edge.example.com:7443",
    oz0.WithAPIKey(os.Getenv("OZ0_API_KEY")), oz0.WithTenant("acme"), oz0.WithCAFile("ca.crt"))
```

`oz0.NewFromEnv()` reads `OZ0_URL`, `OZ0_API_KEY`, `OZ0_TENANT` and `OZ0_CA_FILE`. `WithClientCertificate` takes an operator's certificate, `WithHTTPClient` a client of your own, and `WithConnectOptions` connect-go's options. Calls take their context's deadline.

## Run an agent

```go [main.go]
type review struct {
    Verdict string `json:"verdict"`
}
out, job, err := oz0.RunAs[review](ctx, c, "acme-reviewer", map[string]any{"repo": "acme/api", "pr": 42},
    oz0.WithTimeout(15*time.Minute), oz0.WithBudget(0.5), oz0.WithTags("ci"))
```

`Run` returns the job and a `*oz0.JobFailed` error when it did not complete; `Start`, `Wait` and `Get` are the steps, and `oz0.Output[T]` and `oz0.OutputText` read an answer. Errors from the API are connect-go's, so `connect.CodeOf(err)` is `connect.CodeInvalidArgument` for an input that does not fit the agent's schema.

### Conversations

`oz0.WithContinueFrom(job)` makes the job the next turn of that job's conversation, and `oz0.WithConversation(id)` the next turn of the conversation with that ID (its first, when there is none yet): the agent gets the earlier turns ([Conversations](https://docs.oz0.ai/agents/conversations)). The job says which turn it is.

```go [main.go]
first, err := c.Run(ctx, "acme-assistant", "Our release is on Friday.")
// ...
second, err := c.Run(ctx, "acme-assistant", "When is the release?", oz0.WithContinueFrom(first.GetId()))
fmt.Println(second.GetTurn(), second.GetConversation(), oz0.OutputText(second))
```

`Start` and `Run` retry a start that meets `unavailable` twice, with the same key: without `oz0.WithID`, each start sends an `Idempotency-Key` of its own, so the server gives the retries the same job, and with an ID the retries repeat it. A start again under an ID with the same request returns that job, running or ended; another request under it fails with `already_exists` ([job IDs and retries](https://docs.oz0.ai/build/apps/runtime-api#job-ids-and-retries)).

`oz0.WithCandidate(id)` starts a job on a [private candidate](https://docs.oz0.ai/build/plugins/develop-and-test/candidate-checks#try-it-on-a-real-cluster) instead of the installed version. Every RPC of the API, `GetConversation` and `ListConversations` included, is typed on `c.Runtime`.

## Watch, questions and approvals

```go [main.go]
job, _ := c.Start(ctx, "acme-asker", "Pick a colour for the report")
for ev, err := range c.Watch(ctx, job.GetId()) {
    if err != nil {
        return err
    }
    if a := ev.GetApprovalRequested(); a.GetKind() == "question" {
        _ = c.Answer(ctx, a, "blue", "")
    }
    if f := ev.GetFinished(); f != nil {
        fmt.Println(oz0.OutputText(f))
    }
}
```

`Approvals`, `Questions`, `Approve`, `Deny`, `Answer`, `Cancel`, `Jobs`, `Catalog`, `Cost` and `OpenAPI` do what the [Python SDK's](https://docs.oz0.ai/build/apps/sdk-python#watch-questions-and-approvals) methods do, and `c.Runtime` has every method of the API, typed.

`Watch` resumes a broken stream by itself after the last event it yielded (`since_seq` and the resume token), so no event comes twice; an event with `GetResync()` says a gap could not be resumed over, and the job is fetched again. `WatchFrom(ctx, id, seq, token)` resumes a watch an earlier process made.

## Feeds

`c.OpenFeed(oz0.ConversationFeed(id), oz0.JobFeed(id), oz0.MyActivity())` follows many chats and jobs on one stream; `feed.Events(ctx)` yields its events and resumes it by itself on any edge, `feed.Subscribe` and `feed.Unsubscribe` change what it follows while it runs, and `feed.State()` with `c.ResumeFeed(...)` resumes it in another process. See [Live feeds](https://docs.oz0.ai/build/apps/live-feeds).

## Stream tickets and apps that act as their people

`c.StreamTicket(ctx, oz0.StreamTarget{Job: id})` asks for a one-time ticket that lets a browser's `EventSource` open a job's (or, with `Feed`, a feed's) [Server-Sent Events](https://docs.oz0.ai/build/apps/live-feeds/browser-and-access#stream-tickets) without the key.

A client made with the app's key exchanges a person's ID token for an app token with `c.ExchangeToken(ctx, idToken)` ([apps that act as their people](https://docs.oz0.ai/build/apps/runtime-api/authentication#apps-that-act-as-their-people)); `oz0.WithToken(func(ctx) (string, error))` then makes a client that sends the person's current token on every call. A refused exchange is a `*oz0.TokenExchangeError` with OAuth's `Code`.

The client follows a redirect within the runtime API's origin; one to another origin, which would carry the request's body there (a person's token, a job's input), is an error.

## Lists, page after page

The runtime API's lists that can grow [come a page at a time](https://docs.oz0.ai/reference/list-pages). `AllJobs`, `AllApprovals`, `Conversations`, `Connections` and `RoutineFires` are iterators over every row that read the next page only when the loop reaches it, so a loop that stops early reads no further; `Jobs(ctx, n)` is the first n of `AllJobs` (20 when n is 0 or less), read in pages of n up to 100, and `Approvals` reads every page. `oz0.Pages` does the same for any other list method of `c.Runtime`.

```go
for job, err := range c.AllJobs(ctx, true) { // only the jobs the caller started
    if err != nil {
        return err
    }
    fmt.Println(job.GetId(), job.GetStatus())
}
```

## A person's chats

`MyChats` yields the caller's own [chats](https://docs.oz0.ai/agents/conversations/access-and-history#chats-yours-private-by-default) and those shared with them, page after page; `UpdateChat` pins, archives, marks read, renames (the owner) or removes one from the caller's list, which erases nothing. Both need a person's credentials: an app's key or an operator's certificate has no chats.

```go
for chat, err := range c.MyChats(ctx, oz0.ChatFilter{Agent: "helper"}) {
    if err != nil {
        return err
    }
    fmt.Println(chat.GetId(), chat.GetTitle(), chat.GetUnread())
}
```

## Typed agents

```bash [Terminal]
oz0 client generate --tenant acme --lang go --out internal/agents
```

```go [main.go]
review, job, err := agents.New(c).RunAcmeReviewer(ctx, agents.AcmeReviewerInput{Repo: "acme/api", Pr: 42})
```

The package has a struct per object of the agents' schemas (optional fields as pointers) and per agent and flow a method that starts it and one that runs it, typed. It is named after its directory (`--package` to choose), records the catalog's ETag and the generator's version, and is left alone until the catalog changes or a newer generator writes it again. A property name Go's `encoding/json` cannot carry in a struct tag (a comma, a quote, a back quote, a backslash, an empty name) makes its object a `map[string]any`.

A type that refers to itself works: a required field whose struct would hold, by value, the struct it is in (a node whose `next` is a node) is a pointer too, so the package compiles, and a schema that is a list of itself with no object between is `[]any`.


## Sitemap

See the full [sitemap](https://docs.oz0.ai/sitemap.md) for all pages.
