---
title: Go SDK
description: "Go SDK: the ten functions as methods on a client, with not sure returned as tt.ErrUnsure and failures as errors."
goal: A Go developer installs the SDK, gets a first answer from decide, learns how not sure and failure arrive as errors, and finds the Go call for each function.
reader: A Go developer who wants answers about text inside a service or a batch job
kind: reference
verified: 2026-11-03
---

# Go SDK

The Go SDK gives your code the ten functions as methods on a client; `tt.Answer` turns each result into a typed answer and an error, such as `true` or `false` from decide.

```go
package main

import (
	"context"
	"errors"
	"fmt"
	"log"

	tt "github.com/botassembly/thinkthen/sdk/go/thinkthen"
)

func main() {
	client, err := tt.New(tt.Config{})
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	refundQuestion := tt.NewDecide("Does the customer request a refund?")
	isRefund, err := tt.Answer(client.Decide(context.Background(),
		refundQuestion, tt.Text("Please refund my order.")))
	switch {
	case errors.Is(err, tt.ErrUnsure):
		fmt.Println("not sure: send to a person")
	case err != nil:
		log.Fatal(err)
	default:
		fmt.Println(isRefund)
	}
}
```

Output

```text
true
```


Install it (Go 1.23 or later, with cgo and a C compiler):

```sh
go get github.com/botassembly/thinkthen/sdk/go/thinkthen@v0.2.0
```

The client uses the key you set in the [Quickstart](/quickstart/). Call `client.Close()` when you're done with it.

## When decide is not sure

Give decide a low and a high bar, and a text that falls between them is not sure. `tt.Answer` then returns `tt.ErrUnsure`, and the answer beside it is the zero value, `false`. Check the error before the answer, or not sure reads as a no. This message lands between the bars:

```go
package main

import (
	"context"
	"errors"
	"fmt"
	"log"

	tt "github.com/botassembly/thinkthen/sdk/go/thinkthen"
)

func main() {
	client, err := tt.New(tt.Config{})
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()
	ctx := context.Background()

	refundQuestion := tt.NewDecide(
		"Does the customer request a refund?",
	).WithThreshold(tt.Band(0.2, 0.8))
	message := tt.Text("I was charged twice for one invoice.")
	isRefund, err := tt.Answer(
		client.Decide(ctx, refundQuestion, message))
	switch {
	case errors.Is(err, tt.ErrUnsure):
		fmt.Println("Not sure. Ask a person.")
	case err != nil:
		log.Fatal(err)
	case isRefund:
		fmt.Println("Send to refunds.")
	default:
		fmt.Println("Leave in the inbox.")
	}
}
```

Output

```text
Not sure. Ask a person.
```


Send not sure to a person.

## When a call fails

Blank text fails at once, and nothing is sent. The error is a `*tt.Error`, and its `Code()` names the problem:

```go
package main

import (
	"context"
	"errors"
	"fmt"
	"log"

	tt "github.com/botassembly/thinkthen/sdk/go/thinkthen"
)

func main() {
	client, err := tt.New(tt.Config{})
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()
	ctx := context.Background()

	refundQuestion := tt.NewDecide(
		"Does the customer request a refund?")
	_, err = tt.Answer(
		client.Decide(ctx, refundQuestion, tt.Text("")))
	var failure *tt.Error
	if errors.As(err, &failure) {
		fmt.Println(failure.Code())
	}
}
```

Output

```text
record.empty
```


Any other failure, such as the service being down, comes back as a result whose `Status()` is `tt.Failed`, and `tt.Answer` returns its error. Match a family with `errors.Is(err, tt.ErrRecord)`, or one code with `errors.Is(err, tt.CodeRecordEmpty)`. A problem with the client itself, such as a wrong argument, comes back from `tt.New` or the call. [Troubleshooting](/reference/troubleshooting/) lists each code and its fix.

## The ten functions

Each example asks about the same support-inbox messages as the function pages. Each function has a one-message call, such as `client.Decide`, and an `All` call, such as `client.DecideAll`, that takes many messages and yields a result for each in a `range` loop. In an `All` loop, a failed message arrives as a result, and the loop's own error means the whole call failed.

### decide

<details class="example-disclosure">
<summary>decide: yes, no or not sure</summary>

```go
package main

import (
	"context"
	"errors"
	"fmt"
	"log"

	tt "github.com/botassembly/thinkthen/sdk/go/thinkthen"
)

func main() {
	client, err := tt.New(tt.Config{})
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	refundQuestion := tt.NewDecide("Does the customer request a refund?")
	isRefund, err := tt.Answer(client.Decide(context.Background(),
		refundQuestion, tt.Text("Please refund my order.")))
	switch {
	case errors.Is(err, tt.ErrUnsure):
		fmt.Println("not sure: send to a person")
	case err != nil:
		log.Fatal(err)
	default:
		fmt.Println(isRefund)
	}
}
```

Output

```text
true
```


The message asks for a refund, so decide says `true`. `.WithThreshold(tt.Cutoff(0.8))` moves the bar, and `tt.Band(0.2, 0.8)` sets a low and a high bar, as above. `.WithField("/text")` picks the field to judge when a message is JSON. [decide](/functions/decide/) shows every answer.

</details>

### choose

<details class="example-disclosure">
<summary>choose: one answer from your list</summary>

```go
package main

import (
	"context"
	"fmt"
	"log"

	tt "github.com/botassembly/thinkthen/sdk/go/thinkthen"
)

func main() {
	client, err := tt.New(tt.Config{})
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()
	ctx := context.Background()

	topicQuestion := tt.NewChoose(
		"What is the message about?",
		[]string{"billing", "shipping", "other"})
	message := tt.Text("Please refund my order.")
	topic, err := tt.Answer(
		client.Choose(ctx, topicQuestion, message))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(topic)
}
```

Output

```text
billing
```


A refund request is about billing. With `.WithThreshold(tt.Cutoff(0.8))`, choose is not sure when no choice reaches the bar, and `tt.Answer` returns `tt.ErrUnsure` with an empty string. `client.ChooseAll` reads many messages:

```go
package main

import (
	"context"
	"errors"
	"fmt"
	"log"

	tt "github.com/botassembly/thinkthen/sdk/go/thinkthen"
)

func main() {
	client, err := tt.New(tt.Config{})
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	topicQuestion := tt.NewChoose("What is the message about?",
		[]string{"billing", "shipping", "other"})
	inbox := tt.Texts(
		"Please refund my order.",
		"Where is my parcel?",
		"I was charged twice for one invoice.",
	)
	// ChooseAll sends every message in one call and yields rows in input order.
	for message, err := range client.ChooseAll(
		context.Background(), topicQuestion, inbox,
	) {
		if err != nil {
			log.Fatal(err) // the call failed, not one message
		}
		topic, err := message.Answer()
		switch {
		case errors.Is(err, tt.ErrUnsure):
			fmt.Println(message.Position(), "unsure: send to a person")
		case err != nil:
			// prints the status, code and reason
			fmt.Println(message.Position(), message)
		default:
			fmt.Println(message.Position(), topic)
		}
	}
}
```

Output

```text
1 billing
2 shipping
3 billing
```


[choose](/functions/choose/) shows every answer.

</details>

### filter

<details class="example-disclosure">
<summary>filter: keep the messages that answer yes</summary>

```go
package main

import (
	"context"
	"fmt"
	"log"

	tt "github.com/botassembly/thinkthen/sdk/go/thinkthen"
)

func main() {
	client, err := tt.New(tt.Config{})
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()
	ctx := context.Background()

	refundQuestion := tt.NewFilter(
		"Does the customer request a refund?")
	messages := tt.Texts(
		"Please refund my order.",
		"Where is my parcel?",
		"I was charged twice for one invoice.",
	)
	for kept, err := range client.FilterAll(
		ctx, refundQuestion, messages) {
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println(kept.Item())
	}
}
```

Output

```text
Please refund my order.
I was charged twice for one invoice.
```


`FilterAll` yields the messages that answer yes: the refund request and the double charge. `Item()` is your own message. Not-sure messages are left out. A failed message arrives with its error in its place, and the loop may go on; this example stops on it. `client.FilterRowsAll` yields a result for every message instead, with `Kept()`. [filter](/functions/filter/) shows every answer.

</details>

### tag

<details class="example-disclosure">
<summary>tag: every label that fits</summary>

```go
package main

import (
	"context"
	"fmt"
	"log"

	tt "github.com/botassembly/thinkthen/sdk/go/thinkthen"
)

func main() {
	client, err := tt.New(tt.Config{})
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()
	ctx := context.Background()

	topicsQuestion := tt.NewTag("Which topics appear?",
		[]string{"billing", "shipping"})
	message := tt.Text(
		"I was charged twice and my parcel is late again.")
	topics, err := tt.Answer(
		client.Tag(ctx, topicsQuestion, message))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(topics)
}
```

Output

```text
[billing shipping]
```


The message names a double charge and a late parcel, so both labels fit. The answer is a `[]string`, empty when none fits. [tag](/functions/tag/) shows every answer.

</details>

### score

<details class="example-disclosure">
<summary>score: a number from 0 to 1</summary>

```go
package main

import (
	"context"
	"fmt"
	"log"

	tt "github.com/botassembly/thinkthen/sdk/go/thinkthen"
)

func main() {
	client, err := tt.New(tt.Config{})
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()
	ctx := context.Background()

	urgencyQuestion := tt.NewScore(
		"How urgent is the request?",
		[]string{"routine", "urgent"})
	message := tt.Text("Please refund my order.")
	urgency, err := tt.Answer(
		client.Score(ctx, urgencyQuestion, message))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(urgency)
}
```

Output

```text
0.05
```


A refund request is routine, so it scores near 0. The answer is a `float64`, and levels run lowest first. [score](/functions/score/) shows every answer.

</details>

### rank

<details class="example-disclosure">
<summary>rank: put messages in order</summary>

```go
package main

import (
	"context"
	"fmt"
	"log"

	tt "github.com/botassembly/thinkthen/sdk/go/thinkthen"
)

func main() {
	client, err := tt.New(tt.Config{})
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()
	ctx := context.Background()

	urgencyQuestion := tt.NewRank(
		"How urgent is the request?",
		[]string{"routine", "urgent"})
	messages := tt.Texts(
		"Please refund my order.",
		"Where is my parcel?",
		"I was charged twice for one invoice.",
	)
	for ranked, err := range client.RankAll(
		ctx, urgencyQuestion, messages) {
		if err != nil {
			log.Fatal(err)
		}
		rank, err := ranked.Answer()
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println(rank, ranked.Item())
	}
}
```

Output

```text
1 I was charged twice for one invoice.
2 Where is my parcel?
3 Please refund my order.
```


The double charge is the most urgent, so it ranks 1. The answer is an `int`, and `Item()` is the message it ranks. `.WithTop(2)` ranks only the first two. [rank](/functions/rank/) shows every answer.

</details>

### find

<details class="example-disclosure">
<summary>find: the line that answers</summary>

```go
package main

import (
	"context"
	"fmt"
	"log"

	tt "github.com/botassembly/thinkthen/sdk/go/thinkthen"
)

func main() {
	client, err := tt.New(tt.Config{})
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()
	ctx := context.Background()

	actionQuestion := tt.NewFind(
		"Which line states the requested action?")
	message := tt.Text("Morgan works for Example Group.\n" +
		"Please refund my order.")
	action, err := tt.Answer(
		client.Find(ctx, actionQuestion, message))
	if err != nil {
		log.Fatal(err)
	}
	if action == nil {
		fmt.Println("No line answers.")
	} else {
		fmt.Println(action.Text)
	}
}
```

Output

```text
Please refund my order.
```


The second line holds the request. The answer is a `*tt.Span` with `Text`, `Start` and `End`, counted in bytes, and it is `nil` when no line answers. [find](/functions/find/) shows every answer.

</details>

### recognize

<details class="example-disclosure">
<summary>recognize: names and their kinds</summary>

```go
package main

import (
	"context"
	"fmt"
	"log"

	tt "github.com/botassembly/thinkthen/sdk/go/thinkthen"
)

func main() {
	client, err := tt.New(tt.Config{})
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()
	ctx := context.Background()

	kinds := tt.NewRecognize(
		[]string{"person", "organization"})
	message := tt.Text("Morgan works for Example Group.")
	names, err := tt.Answer(
		client.Recognize(ctx, kinds, message))
	if err != nil {
		log.Fatal(err)
	}
	for _, name := range names {
		fmt.Println(name.Kind, name.Text)
	}
}
```

Output

```text
person Morgan
organization Example Group
```


Morgan is a person and Example Group an organization. Each `tt.Name` also has `Start`, `End` and `Probability`. [recognize](/functions/recognize/) shows every answer.

</details>

### relate

<details class="example-disclosure">
<summary>relate: how names connect</summary>

```go
package main

import (
	"context"
	"fmt"
	"log"

	tt "github.com/botassembly/thinkthen/sdk/go/thinkthen"
)

func main() {
	client, err := tt.New(tt.Config{})
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()
	ctx := context.Background()

	worksFor := tt.NewRelate([]tt.Rule{{
		Name:   "works_for",
		Source: "person",
		Target: "organization",
	}})
	message := tt.Text("Morgan works for Example Group.\n" +
		"Please refund my order.")
	relations, err := tt.Answer(
		client.Relate(ctx, worksFor, message))
	if err != nil {
		log.Fatal(err)
	}
	for _, r := range relations {
		fmt.Println(r.Source.Text, r.Label, r.Target.Text)
	}
}
```

Output

```text
Morgan works_for Example Group
```


Morgan works for Example Group, so relate links the two by the rule. relate finds the names itself, so it needs no recognize call first. Each `tt.Relation` also has `Probability`. [relate](/functions/relate/) shows every answer.

</details>

### annotate

<details class="example-disclosure">
<summary>annotate: several questions at once</summary>

```go
package main

import (
	"context"
	"fmt"
	"log"

	tt "github.com/botassembly/thinkthen/sdk/go/thinkthen"
)

func main() {
	client, err := tt.New(tt.Config{})
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()
	ctx := context.Background()

	form := tt.NewAnnotate(map[string]tt.AnnotationQuestion{
		"refund": tt.NewDecide(
			"Does the customer request a refund?"),
		"topic": tt.NewChoose("What is the message about?",
			[]string{"billing", "shipping", "other"}),
		"urgency": tt.NewScore("How urgent is the request?",
			[]string{"routine", "urgent"}),
	})
	messages := tt.Texts(
		"Please refund my order.",
		"Where is my parcel?",
		"I was charged twice for one invoice.",
	)
	for row, err := range client.AnnotateAll(
		ctx, form, messages) {
		if err != nil {
			log.Fatal(err)
		}
		answers, err := row.Answer()
		if err != nil {
			log.Fatal(err)
		}
		isRefund, err := tt.Answer(answers.Decide("refund"))
		if err != nil {
			log.Fatal(err)
		}
		topic, err := tt.Answer(answers.Choose("topic"))
		if err != nil {
			log.Fatal(err)
		}
		urgency, err := tt.Answer(answers.Score("urgency"))
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println(isRefund, topic, urgency)
	}
}
```

Output

```text
true billing 0.05
false shipping 0.14
true billing 0.45
```


Each message gets all three answers: refund or not, the topic, and its urgency. `answers.Decide("refund")` and the other selectors pick one answer, and `tt.Answer` reads it as a one-question call does, so a not-sure answer inside returns `tt.ErrUnsure` too. `client.Annotate` answers one message. [annotate](/functions/annotate/) shows every answer.

</details>

## The full API

`tt.Config` sets the batch size and retries, and `tt.JSON` and `tt.Records` build messages with ids. The [Go source](https://github.com/botassembly/thinkthen/tree/v0.2.0/sdk/go/thinkthen) lists every name.
