chief-go is the official Go client
for the Chief API. It depends only on the standard library, so you can vendor it
into external tools without pulling in third-party packages.
Install
go get github.com/Storytell-ai/chief-go/chief
Requirements
Go 1.26 or newer. The client depends only on the
standard library, so it adds no third-party transitive dependencies to your
module.
Credentials
A Personal Access Token is required and sent as the X-API-Key header. Most
routes are project-scoped and also need a project id (X-Project-Id). The
Projects.List call is the exception — it works with only an API key and
returns the projects the key can reach.
Create a token in the Chief app under Settings → API tokens — see
Introduction for details on tokens and project
ids.
Pass credentials as options or let the client read them from the environment:
| Option | Environment variable | Required |
|---|
WithAPIKey | CHIEF_API_KEY | yes |
WithProjectID | CHIEF_PROJECT_ID | for project-scoped routes |
WithBaseURL | CHIEF_BASE_URL | no (defaults to https://api.storytell.ai) |
Resources
Each resource is its own service on the client: client.Chats,
client.Assets, client.Labels, client.Actions, client.Sessions,
client.Skills, client.Memories, and client.Projects.
Chats, assets, and labels are available now. Actions, skills, memories, live
sessions, and projects require the latest Chief API release and are rolling
out.
Chats
Chat turns run asynchronously: Create and SendMessage return as soon as the
workflow is accepted. WaitForResponse polls GetMessage until the turn
finishes — or call GetMessage directly to poll on your own schedule.
package main
import (
"context"
"fmt"
"log"
"os"
"time"
"github.com/Storytell-ai/chief-go/chief"
)
func main() {
client, err := chief.New(
chief.WithAPIKey(os.Getenv("CHIEF_API_KEY")),
chief.WithProjectID(os.Getenv("CHIEF_PROJECT_ID")),
)
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
// Open a chat with its first turn; the turn runs asynchronously.
created, err := client.Chats.Create(ctx, &chief.CreateChatRequest{
Prompt: "What changed in the codebase this week?",
})
if err != nil {
log.Fatal(err)
}
// Block until the response is populated.
msg, err := client.Chats.WaitForResponse(ctx, created.ChatID, created.MessageID, 2*time.Minute)
if err != nil {
log.Fatal(err)
}
fmt.Printf("answer: %s\n", msg.Response)
// Ask a follow-up in the same chat.
sent, err := client.Chats.SendMessage(ctx, created.ChatID, &chief.SendMessageRequest{
Prompt: "Now summarize that in one sentence.",
})
if err != nil {
log.Fatal(err)
}
follow, err := client.Chats.WaitForResponse(ctx, created.ChatID, sent.MessageID, 2*time.Minute)
if err != nil {
log.Fatal(err)
}
fmt.Printf("follow-up: %s\n", follow.Response)
}
Assets
UploadFile runs the full three-step flow — create the asset row, PUT the
bytes to the signed URL, then complete the upload to start ingest. The returned
bool is true when the content was a dedup hit and no bytes were uploaded.
// Upload a local file, then block until ingest reaches a terminal status.
asset, deduped, err := client.Assets.UploadFile(ctx, "report.pdf")
if err != nil {
log.Fatal(err)
}
fmt.Printf("asset %q (dedup hit: %t)\n", asset.AssetID, deduped)
asset, err = client.Assets.WaitForReady(ctx, asset.AssetID, 2*time.Minute)
if err != nil {
log.Fatal(err)
}
fmt.Printf("status: %s\n", asset.Status)
// Attach a label by name; an unmatched name is auto-created.
label, err := client.Assets.AttachLabel(ctx, asset.AssetID, "quarterly")
if err != nil {
log.Fatal(err)
}
fmt.Printf("attached label %q (%s)\n", label.Name, label.LabelID)
// List the assets in the project.
page, err := client.Assets.List(ctx, chief.WithLimit(10))
if err != nil {
log.Fatal(err)
}
for _, a := range page.Data {
fmt.Printf("%s\t%s\t%s\n", a.AssetID, a.Status, a.Filename)
}
Actions
Actions run a prompt on a schedule or in response to events, optionally emailing
the result.
// An action that runs every day at 09:00 and emails its result.
action, err := client.Actions.Create(ctx, &chief.ActionRequest{
Name: "daily-digest",
Prompt: "Summarize everything uploaded in the last 24 hours.",
Schedule: &chief.ScheduleRequest{
Hour: "9",
Weekday: "*",
MonthDay: "*",
Timezone: "America/Sao_Paulo",
},
Email: &chief.EmailOutcome{
To: []string{"team@example.com"},
Subject: "Daily digest",
IncludeDateInSubject: true,
},
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("created action %q\n", action.ActionID)
// Pause it, then resume it.
_, _ = client.Actions.Disable(ctx, action.ActionID)
_, _ = client.Actions.Enable(ctx, action.ActionID)
List calls are cursor-paginated. Size a page with WithLimit, then feed the
page’s LastID to WithAfterID to fetch the next one until HasMore is false:
var after string
for {
opts := []chief.ListOption{chief.WithLimit(50)}
if after != "" {
opts = append(opts, chief.WithAfterID(after))
}
page, err := client.Assets.List(ctx, opts...)
if err != nil {
log.Fatal(err)
}
for _, a := range page.Data {
fmt.Println(a.AssetID)
}
if !page.HasMore {
break
}
after = page.LastID
}
WithBeforeID pages backward instead. The two cursors are mutually exclusive.
Errors
Every non-2xx response is returned as an *APIError carrying the HTTP status, a
stable machine-readable Code, and a user-facing Humane message. Helpers
cover the common cases:
session, err := client.Sessions.Get(ctx, "missing-id")
if err != nil {
switch {
case chief.IsNotFound(err):
// 404
default:
if apiErr, ok := chief.IsAPIError(err); ok {
log.Printf("chief: %s (%s)", apiErr.Humane, apiErr.Code)
}
}
}
Debugging
Pass WithDebug(true) when building the client to dump every HTTP request and
response to the standard logger:
client, err := chief.New(
chief.WithAPIKey(os.Getenv("CHIEF_API_KEY")),
chief.WithProjectID(os.Getenv("CHIEF_PROJECT_ID")),
chief.WithDebug(true),
)
Versioning
The package follows semantic versioning. Pin a version in
your go.mod and review the release notes
before upgrading across a major version:
go get github.com/Storytell-ai/chief-go/chief@latest
Reference