DIRTY STATIC / DISPATCH Tutorials12 NOV 2026 β€’ 14 MIN READ
A concrete decision gate in a brutalist hall, with stacks of paper tickets on a conveyor below a wall of caution yellow check marks and dials, and hazard stripes on the gate posts
TutorialsINTELLIGENCE FOR THE MACHINE AGE

Use Gemini Decisions API For Batch Checks

Google launched the Gemini Decisions API on November 11, 2026, powered by a model that writes nothing at all. Gemini Decision 1 takes one shared input and up to 1,000 questions, then returns a probability and a confidence score for each one in a single pass. This tutorial builds a batch checker for support tickets with it. One call asks if a ticket asks for a refund, which team owns it, how severe it is, and whether a person should step in, and the routing code acts only when the confidence earns it.

Start reading ↓

Most support queues run on small judgments. Is this a refund request. Which team owns it. How bad is it. Teams often pay a chat model to answer each judgment in prose, then pay again to parse the prose back into a value the code can use. Google shipped a narrower tool for that job on November 11, 2026. The Gemini Decisions API, powered by the model Gemini Decision 1, generates no text at all. It scores the options you give it and returns numbers.

This tutorial builds a batch checker on that API. One request carries one support ticket and four questions about it. The answers come back as probabilities with confidence scores, and a short routing block sends sure cases onward and holds unsure cases for a person. The steps follow the published documentation and the code examples in the official guide. No live run with a real key is claimed here. The Verification section tells you how to prove the checker works in your own account.

What You Will Build or Learn

You will end with a small Python script that sends one ticket to the Decisions API and gets four judgments back in one call. The script asks if the ticket asks for a refund, which department should handle it, how severe the problem is, and whether a person should review it before anyone acts. It then prints each answer with its confidence and routes the ticket by rules you control.

You will also learn the three question types and the limits that shape any batch design. A yes or no question is type noul in the API. A pick from a fixed set is type choice. A grade on an ordered scale is type score. Each question is judged on its own against the shared input. Adding a question does not change the answers to the others, and the shared input can mix text with images, audio, video and PDF files.

A concrete sorting machine with four caution yellow chutes and probability dials in a brutalist hall One input in, many judgments out. The machine sorts. Your code decides what each pile means. Illustration: Dirty Static

Before You Begin

Decide which judgments in your queue are truly bounded. The API fits questions with a fixed set of answers, such as routing, policy checks, tool selection and grading against a rubric. It cannot explain a decision, pull a value out of a document, or reason through a chain of steps. If a ticket needs a written reply or a chain of thought, that part still belongs to a chat model or a person.

Price shapes the design, so note it before you build. Google charges $0.10 per million input tokens on the paid tier, and output is never billed because the model produces zero output tokens. The shared input is billed once per request no matter how many questions ride on it. There is no context caching and no Batch API support, so a request you repeat bills its input in full each time. Google reports full request times of about 150 to 250 milliseconds, nearly flat from 1 to 100 questions on inputs up to 16K tokens. Those are Google figures. Measure them on your own tickets before you design around them.

Step 1: Get a Key and Install the SDK

Action: open Google AI Studio, create an API key, and store it in an environment variable with a placeholder name in your notes. Then install the SDK at version 3.2 or later. Older versions do not carry the decisions namespace at all.

macOS or Linux Β· Terminal

export GEMINI_API_KEY="YOUR_API_KEY"
pip install -U google-genai

Expected result: the variable is set for your shell session and the install reports google-genai at version 3.2 or later. You can also try questions with no code in the Decisions Playground in AI Studio, which exports runnable code when you are ready.

Step 2: Ask One Yes or No Question

Action: start with the smallest call the guide gives. The client must name the v1beta API version, or the decisions namespace does not appear. This example uses the damaged mug text from the official guide.

from google import genai

client = genai.Client(http_options={"api_version": "v1beta"})

result = client.decisions.create(
    model="gemini-decision-1",
    input="The blue mug I ordered on Monday arrived cracked on Wednesday.",
    questions={
        "is_damaged": {
            "type": "noul",
            "instructions": "Did the item arrive damaged?",
        },
    },
)

answer = result.answers["is_damaged"]
print(f"P(yes)={answer.noul:.2f} confidence={answer.confidence:.2f}")

Expected result: the call returns an answers map under the same key you sent, plus a usage block where output tokens read zero. The guide shows a noul value of 0.99 with a confidence of 0.99 for this text. The noul field is the probability that the answer is yes, and it always falls between 0.01 and 0.99. Confidence for this type is the larger of the value and one minus the value, so a value of 0.03 carries a confidence of 0.97 that the answer is no. The same call over REST goes to POST https://generativelanguage.googleapis.com/v1beta/decisions with your key in the x-goog-api-key header. Note the path. This model does not answer on the generateContent path that other Gemini models use.

Step 3: Turn One Call Into a Batch Check

Action: keep the same client and grow the questions map. This batch runs four checks on one ticket. It mixes all three types, and it adds a scope check that asks if the message concerns an order at all. That scope check matters in Step 5.

ticket = "I was charged twice for my order and the export page crashes in Safari."

result = client.decisions.create(
    model="gemini-decision-1",
    input=ticket,
    questions={
        "about_order": {
            "type": "noul",
            "instructions": "Is this message about an order, a payment or a delivery?",
        },
        "is_refund": {
            "type": "noul",
            "instructions": "Does the customer ask for a refund or a charge reversal?",
        },
        "department": {
            "type": "choice",
            "instructions": "Which department should handle this request?",
            "criteria": {
                "billing": "Payments, invoices, and refunds.",
                "technical": "Problems using the product.",
                "shipping": "Delivery and tracking.",
                "other": "Anything outside these categories.",
            },
        },
        "severity": {
            "type": "score",
            "instructions": "How severe is this issue for the user?",
            "criteria": [
                "Cosmetic: appearance only, no lost functionality.",
                "Workaround available: a task fails, but another way works.",
                "Fully blocked: a task fails with no workaround.",
            ],
        },
    },
)

Expected result: one response holds four answers, one per key. The choice answer names the top option and gives a probability for every option. The score answer gives a weighted average across the levels, so it can land between levels, and it carries a probability for each level plus a legend that maps level numbers to your level text. Three facts from the docs shape how you write these questions. The model never sees your question keys, so the full question must live in the instructions text. Keys must run from 1 to 64 characters drawn from letters, digits, underscores and hyphens. The API sorts choice options by key before it scores them, and a tie on exact probability goes to the option whose key sorts first. Name your safety net option other, as in this example. A name such as none_of_the_above sorts ahead of real options and can win ties it should lose.

Step 4: Add a Photo or a PDF to the Same Input

Action: when a check needs more than the ticket text, send the input as a list of blocks instead of a string. Blocks can be text, image, audio, video or document, and document means PDF only. Set the true media type on each block, because the API rejects application/octet-stream. Inline data is capped at 100 MB per request, and at 50 MB for PDF files. Files at external URLs are capped at 15 MB each, and YouTube links and gs:// addresses are not supported. Use the Files API, a public or signed HTTPS address, or inline bytes instead.

import base64

with open("photo.jpg", "rb") as f:
    image_b64 = base64.b64encode(f.read()).decode("utf-8")

result = client.decisions.create(
    model="gemini-decision-1",
    input=[
        {"type": "text", "text": "Customer photo for return 8821.\n"},
        {"type": "image", "data": image_b64, "mime_type": "image/jpeg", "resolution": "low"},
    ],
    questions={
        "visible_damage": {
            "type": "noul",
            "instructions": "Does the photo show physical damage to the item? Ignore the packaging.",
        },
    },
)

Expected result: the photo joins the text as one shared input, and every question in the same request can judge both. The resolution setting trades visual detail for tokens, with values low, medium, high and ultra_high, and the last of those serves images only. A batch can grow to 1,000 questions per request, inside a total limit of 262,144 tokens shared by the input and the questions.

Step 5: Route on Probability and Confidence

Action: read the value to pick an action and read the confidence to decide if the action may run on its own. The numbers below are starting points from the guide examples. Replace them with cutoffs you earn in Step 6.

a = result.answers

dept = a["department"]
if a["about_order"].noul < 0.5:
    route_to("general_intake")
elif dept.confidence >= 0.7 and dept.choice != "other":
    route_to(dept.choice)
elif dept.confidence >= 0.4:
    route_to_fallback_model(ticket)
else:
    route_to("human_review")

Expected result: sure cases move on, middling cases get a second look from a larger model that can reason, and weak cases wait for a person. Two rules from the docs protect this block. A choice question always splits its probability across the options you gave it, even when none of them fits, which is why the scope check runs first in this code. A score value is an average, not a level. When you need one level, take the level with the highest probability and read its text from the legend, as this short block does.

sev = result.answers["severity"]
level = max(sev.probabilities, key=sev.probabilities.get)
print(sev.legend[level])

Step 6: Test With Your Own Labeled Tickets

Action: build a small set of past tickets where a person has already recorded the right department, the right severity and the refund call. Run the batch on each one and compare the top answers with the labels. Count how often each confidence band proves right, then set your routing cutoffs from those counts. Google advises exactly this. Thresholds should come from labeled examples of your own traffic, and the same set tells you if the probabilities mean what they claim on your data.

Expected result: you hold a measured cutoff for each route, plus a list of tickets the batch gets wrong that you can study. Keep the set and rerun it after any edit to instructions or criteria. A safety block needs care in this step too. When the input filter blocks a request, the API returns HTTP 400 with an error code such as safety or prohibited_content and no answers at all. Record that outcome as no decision and send the ticket to a person. A blocked ticket is often the one that most needs human eyes.

A reviewer at a desk with a large magnifying lens, studying a pile of held tickets beside a conveyor, with caution yellow warning lights on the wall The pile that waits for a person is a feature of the design, not a failure of it. Illustration: Dirty Static

Verification

Run these checks and record the results. The script sends one request for a single ticket and the usage block reports zero output tokens. The answers map holds every key you sent, and each answer carries a confidence between 0 and 1. The mug text from Step 2 returns a high noul value, and a ticket with no order content routes to general intake through the scope check. Your labeled set from Step 6 passes at the rate your cutoffs assume, and a blocked input lands in human review rather than in a queue.

Finish with the cost check. Price one request by hand from its input token count at $0.10 per million tokens, then compare that figure with the cost of the same checks sent as separate chat calls that resend the ticket each time. The gap between those two figures is the whole reason this endpoint exists.

Troubleshooting

ProblemLikely causeFix
The decisions namespace does not appear on the clientThe SDK sits below version 3.2, or the client lacks the v1beta settingInstall google-genai at 3.2 or later and create the client with the v1beta API version, as in Step 2
A REST call returns 404 or a model errorThe call went to the generateContent path or to version v1Send the request to POST /v1beta/decisions with the model gemini-decision-1, as in Step 2
A threshold rule never firesThe cutoff sits above 0.99 or below 0.01, and noul values never leave that rangeKeep yes or no cutoffs inside 0.01 to 0.99 and set them from your labeled set
The department answer picks an option when none fitsA choice question must spread probability across the options it was givenRun a scope check of type noul first, as in Steps 3 and 5, and route failures away before you trust the choice
A score prints as a fraction and looks wrongThe score field is a weighted average across levels, not a level indexRead the level with the highest probability and print its legend text, as in Step 5
The whole request returns HTTP 400 with no answersA safety filter blocked the input, and the error code names the blockTreat the result as no decision and send the item to human review. Never read a block as a no on every question
Costs rise on repeat runs of the same inputThe API offers no context caching, so each request bills the shared input in fullBatch more questions into each request and cut repeat calls on inputs that have not changed

Next Steps

Grow the batch where the shape fits. A contract or a screenshot can face dozens of policy rules in one request. A router can ask which tool fits a task and which model tier should take it, the same control flow pattern our guide to sandboxed agent sessions guards from the other side. Keep each new question independent, add it to the labeled set first, and let its answers earn trust at low stakes before they move money or close tickets.

The habit worth keeping is the split this API forces. Judgment calls that end in a fixed set of answers go to the batch. Work that needs words, reasons or a chain of steps goes elsewhere. Queues get faster when each kind of work runs on the tool that was built for it.

Facts checked against the Gemini Decisions API documentation and guide examples on November 12, 2026. Prices, limits and latency figures are Google published values and can change, so confirm them in the docs before a wider rollout. This procedure was verified against those documents and is not presented as a personal test run.

Sources

β– 

END OF TRANSMISSION

Stay curious. Question everything.

CONTINUE THE DESCENT

MORE FROM DIRTY STATIC β†˜