---
title: Zig SDK
description: "Zig SDK: ask the ten ThinkThen questions from Zig and get yes, no or not sure back as plain Zig values."
goal: The reader runs a first decide in Zig, adds the SDK to a build, handles not sure and failures, and finds the Zig call for each of the ten functions.
reader: A Zig developer who wants to ask questions about text from code
kind: reference
verified: 2026-11-03
sidebar:
  label: "Zig"
---

# Zig SDK

The Zig SDK gives your Zig code the ten functions as methods on one client; each call gives back a result whose `answer` is a plain Zig value, such as `true` or `false` from decide, and returns `error.Unsure` when the answer is not sure.

```zig
const std = @import("std");
const tt = @import("thinkthen");

pub fn main(init: std.process.Init) !void {
    var buffer: [4096]u8 = undefined;
    var stdout = std.Io.File.stdout().writer(init.io, &buffer);
    const out = &stdout.interface;
    defer out.flush() catch {};
    var diag: tt.Diagnostic = .{};
    var client = try tt.Client.init(.{}, &diag);
    defer client.deinit();

    // answer returns error.Unsure when not sure, and the message's error
    // when it failed.
    const is_refund = try (try client.decide(
        init.arena.allocator(),
        "Does the customer request a refund?",
        .{ .text = "Please refund my order." },
        .{},
        &diag,
    )).answer(&diag);
    try out.print("{}\n", .{is_refund});
}
```

Output

```text
true
```


`true` means yes: the message asks for a refund.

## Add it to your build

The SDK needs Zig 0.17.0. In a project that has a `build.zig.zon`, such as one `zig init` made, fetch the archive for your machine:

```sh
release=https://github.com/botassembly/thinkthen/releases/\
download/v0.2.0
zig fetch --save=thinkthen \
  "$release/thinkthen-zig-0.2.0-linux-x86_64.tar.gz"
```

Archives are published for `linux-x86_64`, `linux-arm64` and `macos-arm64`; change the end of the address to match your machine. `zig fetch --save` adds the dependency to `build.zig.zon`.

This `build.zig` builds `src/main.zig`, adds the `thinkthen` module, installs the native library beside your program with `installNative`, and adds a `run` step:

<details class="example-disclosure">
<summary>build.zig</summary>

```zig title="build.zig"
const std = @import("std");
const thinkthen = @import("thinkthen");

pub fn build(b: *std.Build) !void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});
    const dependency = b.dependency("thinkthen", .{
        .target = target,
        .optimize = optimize,
    });
    const executable = b.addExecutable(.{
        .name = "example",
        .root_module = b.createModule(.{
            .root_source_file = b.path("src/main.zig"),
            .target = target,
            .optimize = optimize,
            .link_libc = true,
        }),
    });
    executable.root_module.addImport("thinkthen",
        dependency.module("thinkthen"));
    const installed = b.addInstallArtifact(executable, .{});
    // Installs the package's native library beside the executable.
    try thinkthen.installNative(b, dependency, installed);
    b.getInstallStep().dependOn(&installed.step);
    const run = b.addRunArtifact(executable);
    run.step.dependOn(&installed.step);
    b.step("run", "Run the example").dependOn(&run.step);
}
```

</details>

Save the program as `src/main.zig` and run `zig build run`. The client uses the key from the [Quickstart](/quickstart/).

## Handle not sure and failures

Set a low bar and a high bar with `tt.Threshold.band`. Between them, decide is not sure. Each result's `outcome` is a union of `.answered`, `.unsure` and `.failed`, so a `switch` handles all three:

```zig
const std = @import("std");
const tt = @import("thinkthen");

pub fn main(init: std.process.Init) !void {
    var buffer: [4096]u8 = undefined;
    var stdout = std.Io.File.stdout().writer(init.io, &buffer);
    const out = &stdout.interface;
    defer out.flush() catch {};
    var diag: tt.Diagnostic = .{};
    var client = try tt.Client.init(.{}, &diag);
    defer client.deinit();

    const refund = try client.decide(
        init.arena.allocator(),
        "Does the customer request a refund?",
        .{ .text = "I was charged twice for one invoice." },
        .{ .threshold = tt.Threshold.band(0.2, 0.8) },
        &diag,
    );
    switch (refund.outcome) {
        .answered => |answered| try out.print(
            "{}\n",
            .{answered.answer},
        ),
        .unsure => |unsure| try out.print(
            "Not sure ({d}): ask a person.\n",
            .{unsure.probability},
        ),
        .failed => |failure| try out.print(
            "{s}: {s}\n",
            .{ failure.name(), failure.message },
        ),
    }
}
```

Output

```text
Not sure (0.7): ask a person.
```


A double charge might be a refund request, or might not, so the answer is not sure. Send it to a person.

`try result.answer(&diag)` is the short way to the answer. It returns `error.Unsure` when not sure, with the probability in `diag.probability`. When ThinkThen can't judge the message, it returns that message's error, such as `error.Record` or `error.Backend`, and `diag.code` and `diag.message()` say why. Neither one can pass for `false`. A problem with the client or the call itself, such as a wrong argument, comes back as an error from `tt.Client.init` or the call, such as `error.Usage`, with `diag` filled the same way. [Troubleshooting](/reference/troubleshooting/) lists each error code.

## The ten functions

Each function reads the same support-inbox messages as the function pages. A one-message call, such as `client.decide`, takes an arena and gives back one result. A many-message call, such as `client.filterAll`, takes an allocator and an input such as `.{ .texts = &messages }`; read it with `while (try call.next(&diag)) |item|` and free it with `deinit`.

### decide

<details class="example-disclosure">
<summary>Yes or no about one message</summary>

```zig
const std = @import("std");
const tt = @import("thinkthen");

pub fn main(init: std.process.Init) !void {
    var buffer: [4096]u8 = undefined;
    var stdout = std.Io.File.stdout().writer(init.io, &buffer);
    const out = &stdout.interface;
    defer out.flush() catch {};
    var diag: tt.Diagnostic = .{};
    var client = try tt.Client.init(.{}, &diag);
    defer client.deinit();

    // answer returns error.Unsure when not sure, and the message's error
    // when it failed.
    const is_refund = try (try client.decide(
        init.arena.allocator(),
        "Does the customer request a refund?",
        .{ .text = "Please refund my order." },
        .{},
        &diag,
    )).answer(&diag);
    try out.print("{}\n", .{is_refund});
}
```

Output

```text
true
```


Yes: the message asks for a refund. `DecideOptions` takes `.threshold` (one bar, or a low and a high bar with `tt.Threshold.band`), `.yes_means` and `.no_means`. `client.decideAll` takes many messages and gives one result each, in order. [decide](/functions/decide/) shows every option.

</details>

### choose

<details class="example-disclosure">
<summary>The one choice that fits</summary>

```zig
const std = @import("std");
const tt = @import("thinkthen");

pub fn main(init: std.process.Init) !void {
    var buffer: [4096]u8 = undefined;
    var stdout = std.Io.File.stdout().writer(init.io, &buffer);
    const out = &stdout.interface;
    defer out.flush() catch {};
    var diag: tt.Diagnostic = .{};
    var client = try tt.Client.init(.{}, &diag);
    defer client.deinit();

    const topic = try (try client.choose(
        init.arena.allocator(),
        "What is the message about?",
        tt.choices(&.{ "billing", "shipping", "other" }),
        .{ .text = "Please refund my order." },
        .{},
        &diag,
    )).answer(&diag);
    try out.print("{s}\n", .{topic});
}
```

Output

```text
billing
```


A refund is about billing. `tt.choices` builds the list at compile time. `ChooseOptions` takes `.descriptions`, from each choice to what it means, and `.threshold`, which can make choose not sure. [choose](/functions/choose/) shows every option.

</details>

### filter

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

```zig
const std = @import("std");
const tt = @import("thinkthen");

pub fn main(init: std.process.Init) !void {
    var buffer: [4096]u8 = undefined;
    var stdout = std.Io.File.stdout().writer(init.io, &buffer);
    const out = &stdout.interface;
    defer out.flush() catch {};
    var diag: tt.Diagnostic = .{};
    var client = try tt.Client.init(.{}, &diag);
    defer client.deinit();

    const messages = [_][]const u8{
        "Please refund my order.",
        "Where is my parcel?",
        "I was charged twice for one invoice.",
    };
    // next yields only the kept messages.
    // A failed message returns its error in its place.
    var refunds = try client.filterAll(
        init.gpa,
        "Does the customer request a refund?",
        .{ .texts = &messages },
        .{},
        &diag,
    );
    defer refunds.deinit();
    while (try refunds.next(&diag)) |refund|
        try out.print("{s}\n", .{refund.text});
}
```

Output

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


The parcel question is left out. `next` gives back only the kept messages, and skips the ones it is not sure about. A message that fails returns its error in its place, with its position in `diag.position`, and reading can go on after it. `nextRow` gives every message with its outcome instead. [filter](/functions/filter/) shows every option.

</details>

### tag

<details class="example-disclosure">
<summary>Every label that fits</summary>

```zig
const std = @import("std");
const tt = @import("thinkthen");

pub fn main(init: std.process.Init) !void {
    var buffer: [4096]u8 = undefined;
    var stdout = std.Io.File.stdout().writer(init.io, &buffer);
    const out = &stdout.interface;
    defer out.flush() catch {};
    var diag: tt.Diagnostic = .{};
    var client = try tt.Client.init(.{}, &diag);
    defer client.deinit();

    // Every topic that fits, which may be none.
    const topics = try (try client.tag(
        init.arena.allocator(),
        "Which topics appear?",
        tt.choices(&.{ "billing", "shipping" }),
        .{ .text = "I was charged twice and my parcel is late again." },
        .{},
        &diag,
    )).answer(&diag);
    for (topics) |topic| try out.print("{s}\n", .{topic});
}
```

Output

```text
billing
shipping
```


The message is about both billing and shipping. The answer is a slice of labels, empty when no label fits. [tag](/functions/tag/) shows every option.

</details>

### score

<details class="example-disclosure">
<summary>A number from 0 to 1 on your scale</summary>

```zig
const std = @import("std");
const tt = @import("thinkthen");

pub fn main(init: std.process.Init) !void {
    var buffer: [4096]u8 = undefined;
    var stdout = std.Io.File.stdout().writer(init.io, &buffer);
    const out = &stdout.interface;
    defer out.flush() catch {};
    var diag: tt.Diagnostic = .{};
    var client = try tt.Client.init(.{}, &diag);
    defer client.deinit();

    // From 0 (routine) to 1 (urgent).
    const urgency = try (try client.score(
        init.arena.allocator(),
        "How urgent is the request?",
        tt.choices(&.{ "routine", "urgent" }),
        .{ .text = "Please refund my order." },
        .{},
        &diag,
    )).answer(&diag);
    try out.print("{d}\n", .{urgency});
}
```

Output

```text
0.05
```


The levels run lowest first, so a plain refund request scores near routine. The answer is an `f64`. [score](/functions/score/) shows every option.

</details>

### rank

<details class="example-disclosure">
<summary>Every message, highest first</summary>

```zig
const std = @import("std");
const tt = @import("thinkthen");

pub fn main(init: std.process.Init) !void {
    var buffer: [4096]u8 = undefined;
    var stdout = std.Io.File.stdout().writer(init.io, &buffer);
    const out = &stdout.interface;
    defer out.flush() catch {};
    var diag: tt.Diagnostic = .{};
    var client = try tt.Client.init(.{}, &diag);
    defer client.deinit();

    const messages = [_][]const u8{
        "Please refund my order.",
        "Where is my parcel?",
        "I was charged twice for one invoice.",
    };
    // Every message, most urgent first. The answer is its rank, starting at 1.
    var by_urgency = try client.rankAll(
        init.gpa,
        "How urgent is the request?",
        tt.choices(&.{ "routine", "urgent" }),
        .{ .texts = &messages },
        .{},
        &diag,
    );
    defer by_urgency.deinit();
    while (try by_urgency.next(&diag)) |ranked| {
        const rank = try ranked.answer(&diag);
        try out.print("{d} {s}\n", .{
            rank,
            messages[@intCast(ranked.position - 1)],
        });
    }
}
```

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. Each result's answer is its rank, a `u64` starting at 1, and `position` is its place in your input, counting from 1. `RankOptions` takes `.top` to keep only the first few. [rank](/functions/rank/) shows every option.

</details>

### find

<details class="example-disclosure">
<summary>The line that answers the question</summary>

```zig
const std = @import("std");
const tt = @import("thinkthen");

pub fn main(init: std.process.Init) !void {
    var buffer: [4096]u8 = undefined;
    var stdout = std.Io.File.stdout().writer(init.io, &buffer);
    const out = &stdout.interface;
    defer out.flush() catch {};
    var diag: tt.Diagnostic = .{};
    var client = try tt.Client.init(.{}, &diag);
    defer client.deinit();

    // null when no line answers.
    const action = try (try client.find(
        init.arena.allocator(),
        "Which line states the requested action?",
        .{ .text = "Morgan works for Example Group.\nPlease refund my order." },
        .{},
        &diag,
    )).answer(&diag);
    if (action) |line|
        try out.print("{s}\n", .{line.text})
    else
        try out.print("no line answers\n", .{});
}
```

Output

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


The second line states the action. The answer is a `?tt.Span` with `text`, `start` and `end`, or `null` when no line answers. Offsets count UTF-8 bytes. [find](/functions/find/) shows every option.

</details>

### recognize

<details class="example-disclosure">
<summary>Each name in the text and its kind</summary>

```zig
const std = @import("std");
const tt = @import("thinkthen");

pub fn main(init: std.process.Init) !void {
    var buffer: [4096]u8 = undefined;
    var stdout = std.Io.File.stdout().writer(init.io, &buffer);
    const out = &stdout.interface;
    defer out.flush() catch {};
    var diag: tt.Diagnostic = .{};
    var client = try tt.Client.init(.{}, &diag);
    defer client.deinit();

    const names = try (try client.recognize(
        init.arena.allocator(),
        tt.choices(&.{ "person", "organization" }),
        .{ .text = "Morgan works for Example Group." },
        .{},
        &diag,
    )).answer(&diag);
    for (names) |name| try out.print("{s}: {s}\n", .{ name.kind, name.text });
}
```

Output

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


The answer is a slice of `tt.Name`, each with `text`, `kind`, `start`, `end` and `probability`. [recognize](/functions/recognize/) shows every option.

</details>

### relate

<details class="example-disclosure">
<summary>Names linked by the relations you allow</summary>

```zig
const std = @import("std");
const tt = @import("thinkthen");

pub fn main(init: std.process.Init) !void {
    var buffer: [4096]u8 = undefined;
    var stdout = std.Io.File.stdout().writer(init.io, &buffer);
    const out = &stdout.interface;
    defer out.flush() catch {};
    var diag: tt.Diagnostic = .{};
    var client = try tt.Client.init(.{}, &diag);
    defer client.deinit();

    const works_for = [_]tt.RelationRule{.{
        .name = "works_for",
        .source = "person",
        .target = "organization",
    }};
    const relations = try (try client.relate(
        init.arena.allocator(),
        &works_for,
        .{ .text = "Morgan works for Example Group.\nPlease refund my order." },
        .{},
        &diag,
    )).answer(&diag);
    for (relations) |relation| try out.print("{s} --{s}--> {s}\n", .{
        relation.source.text,
        relation.relation,
        relation.target.text,
    });
}
```

Output

```text
Morgan --works_for--> Example Group
```


A `tt.RelationRule` names the kind of name at each end and the relation between them. Each `tt.Relation` in the answer has a `source`, a `target` and the `relation`. [relate](/functions/relate/) shows every option.

</details>

### annotate

<details class="example-disclosure">
<summary>Several questions for each message</summary>

```zig
const std = @import("std");
const tt = @import("thinkthen");

pub fn main(init: std.process.Init) !void {
    var buffer: [4096]u8 = undefined;
    var stdout = std.Io.File.stdout().writer(init.io, &buffer);
    const out = &stdout.interface;
    defer out.flush() catch {};
    var diag: tt.Diagnostic = .{};
    var client = try tt.Client.init(.{}, &diag);
    defer client.deinit();

    const form: tt.AnnotationQuestions = .{ .entries = &.{
        .{ .name = "refund", .question = .{ .decide = .{
            .question = "Does the customer request a refund?",
        } } },
        .{ .name = "topic", .question = .{ .choose = .{
            .question = "What is the message about?",
            .choices = tt.choices(&.{ "billing", "shipping", "other" }),
        } } },
        .{ .name = "urgency", .question = .{ .score = .{
            .question = "How urgent is the request?",
            .levels = tt.choices(&.{ "routine", "urgent" }),
        } } },
    } };
    const messages = [_][]const u8{
        "Please refund my order.",
        "Where is my parcel?",
        "I was charged twice for one invoice.",
    };
    var triage = try client.annotateAll(
        init.gpa,
        form,
        .{ .texts = &messages },
        .{},
        &diag,
    );
    defer triage.deinit();
    while (try triage.next(&diag)) |filled| {
        // Each answer can be not sure or failed on its own, and answer says so.
        const answers = try filled.answer(&diag);
        const is_refund = try answers.get(.decide, "refund").?.answer(&diag);
        const topic = try answers.get(.choose, "topic").?.answer(&diag);
        const urgency = try answers.get(.score, "urgency").?.answer(&diag);
        try out.print("{} {s} {d}\n", .{ is_refund, topic, urgency });
    }
}
```

Output

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


`answers.get(.decide, "refund")` reads one answer by its function and name, and gives `null` for a name you never added. Each answer can be not sure or fail on its own, so its `answer` returns `error.Unsure` or its error, as decide does. For one message, call `client.annotate`. [annotate](/functions/annotate/) shows every option.

</details>

## The full API

Each options type takes `.field`, a pointer such as `"/text"`, to judge one field of a JSON message passed as `.{ .json = ... }`. `tt.Client.init` takes `tt.Settings` to set the batch size and retries. A many-message call's `cancel` stops it from any thread. The [Zig API reference](https://github.com/botassembly/thinkthen/blob/v0.2.0/sdk/zig/src/thinkthen.zig) comments every public name.
