# How to use the Gemini Decisions API

> The Gemini Decisions API answers yes/no, choice and score questions with probabilities instead of text. Here's how to call it from Node.js and branch on the result.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2026-11-11 | Topics: [AI](https://flaviocopes.com/tags/ai/) | Canonical: https://flaviocopes.com/gemini-decisions-api/

Google released **Gemini Decision 1** on November 11, 2026, with a new endpoint to call it, the **Decisions API**.

It's a model that doesn't write text. You send it some input and a list of questions, and it returns a probability for each answer. Your code reads the numbers and decides what to do.

I haven't run it yet. The requests and responses below come from Google's docs.

## The problem it solves

A lot of the calls we make to an LLM are not about generating text. We ask "is this spam?", "which queue does this go to?", "how urgent is it?", and then our code branches on the answer.

With a normal model you write a prompt, ask for JSON, parse it, and hope the model answered with one of the values you listed. You also pay for output tokens and wait for them to be generated.

The Decisions API skips that part. There are no output tokens at all, and the answer is already typed.

```mermaid
flowchart LR
  accTitle: A Decisions API call
  accDescr: Your app sends shared input and a map of questions to Gemini Decision 1. The model returns one answer per question with a probability and a confidence, and your code branches on those numbers.
  A[Input: text, image, audio, video, PDF] --> C[gemini-decision-1]
  B[Questions: noul, choice, score] --> C
  C --> D[Answers with probabilities]
  D --> E{Your code}
  E -->|confidence high| F[Run the action]
  E -->|confidence low| G[Send to a human]
```

## What it costs

You pay $0.10 per 1M input tokens, for any kind of input. Output is always 0 tokens.

The free tier allows 200 requests per minute and 10,000 per day, which is a lot for a free tier. Be careful with one thing: on the free tier Google uses your data to improve its products. On the paid tier it doesn't.

The context window is 262,144 tokens, shared between the input and all the questions.

## Setup

You need version 3.2 or later of the Google GenAI SDK:

```bash
npm install @google/genai@latest
```

Get an API key in [Google AI Studio](https://aistudio.google.com/) and export it. The SDK reads it from the environment:

```bash
export GEMINI_API_KEY=your-key-here
```

## The first request

A request has three parts: the `model`, the `input`, and a map of `questions`. You pick the keys of the map, and the answers come back under the same keys.

Save this as `decisions.mjs` and run it with `node decisions.mjs`:

```js
import { GoogleGenAI } from '@google/genai'

const ai = new GoogleGenAI({ apiVersion: 'v1beta' })

const result = await ai.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?'
    }
  }
})

console.log(JSON.stringify(result, null, 2))
```

This is the response:

```json
{
  "answers": {
    "is_damaged": { "type": "noul", "noul": 0.99, "confidence": 0.99 }
  },
  "usage": {
    "total_input_tokens": 100,
    "input_tokens_by_modality": [{ "modality": "text", "tokens": 100 }],
    "total_output_tokens": 0,
    "total_tokens": 100
  }
}
```

`noul` is the probability that the answer is yes. Notice `total_output_tokens` is 0.

The model never sees the key `is_damaged`. It only sees the `instructions`, so write the full question there.

There is no SDK requirement, by the way. It's one POST request, so you can call it with `curl`:

```bash
curl -X POST "https://generativelanguage.googleapis.com/v1beta/decisions" \
  -H "x-goog-api-key: $GEMINI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "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?"
      }
    }
  }'
```

## The three question types

Every question has a `type`, and the type decides what `criteria` looks like and what you get back.

| Type | You ask | `criteria` | You get back |
| --- | --- | --- | --- |
| `noul` | A yes/no question | Optional: what yes and no mean | `noul` (probability of yes), `confidence` |
| `choice` | Pick one option from a list | Required: a map of option to description | `choice`, `probabilities`, `confidence` |
| `score` | Rate on a scale | Required: an array of levels, lowest first | `score`, `probabilities`, `legend`, `confidence` |

### Yes or no

We saw `noul` in the first request. When the question alone is ambiguous, add `criteria` to say what counts as yes and what counts as no:

```js
const result = await ai.decisions.create({
  model: 'gemini-decision-1',
  input: 'Customer returned the sweater because they preferred green over blue.',
  questions: {
    is_defect: {
      type: 'noul',
      instructions: 'Is the customer reporting a product defect?',
      criteria: {
        true_description: 'The product itself is broken or faulty.',
        false_description: 'Preference, sizing, shipping, or pricing complaint.'
      }
    }
  }
})
```

The answer is a clear no:

```json
{ "type": "noul", "noul": 0.03, "confidence": 0.97 }
```

The probability is always between 0.01 and 0.99. If you write `noul > 0.99` in your code, it never matches.

### Pick one option

A `choice` question takes a map of options. The key is the value you want back, and the value describes when that option applies:

```js
const result = await ai.decisions.create({
  model: 'gemini-decision-1',
  input: 'The blue mug I ordered on Monday arrived cracked on Wednesday.',
  questions: {
    queue: {
      type: 'choice',
      instructions: 'Which support queue should handle this message?',
      criteria: {
        billing: 'Payment, invoice, or refund questions.',
        replacements: 'Damaged, missing, or wrong items.',
        general: 'Product questions and account updates.'
      }
    }
  }
})

console.log(result.answers.queue.choice)
```

The answer has the winning option in `choice`, plus a `probabilities` map with one number per option. The numbers sum to 1.

Be careful with inputs that fit none of the options. The model always spreads the probability across your list, so a message about the weather still gets a queue. You can add an `other` option with a description, or check `confidence` before acting.

You can have from 2 to 255 options.

### Rate on a scale

A `score` question takes an array of levels, ordered from lowest to highest:

```js
const result = await ai.decisions.create({
  model: 'gemini-decision-1',
  input: 'Export fails in Safari but works in Chrome.',
  questions: {
    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.'
      ]
    }
  }
})
```

Levels are indexed from 0. Here's the answer:

```json
{
  "type": "score",
  "score": 1.12,
  "legend": {
    "0": "Cosmetic: appearance only, no lost functionality.",
    "1": "Workaround available: a task fails, but another way works.",
    "2": "Fully blocked: a task fails with no workaround."
  },
  "probabilities": { "0": 0.03, "1": 0.83, "2": 0.15 },
  "confidence": 0.83
}
```

The `score` is 1.12, which is not one of the levels. It's the average of the level indexes, weighted by their probabilities.

That's useful to sort a list of items. When you need one level, take the one with the highest probability:

```js
const { probabilities, legend } = result.answers.severity
const level = Object.keys(probabilities).reduce((a, b) =>
  probabilities[a] >= probabilities[b] ? a : b
)

console.log(legend[level]) //Workaround available: a task fails, but another way works.
```

## Ask many questions in one request

You can put up to 1,000 questions in the same request, and mix the types.

The input is counted once, no matter how many questions you ask. Each question only adds the tokens of its own instructions and criteria.

The questions also can't see each other. Adding or removing one doesn't change the other answers.

Let's pass an order and a customer message as two text blocks, and ask three questions about them:

```js
import { GoogleGenAI } from '@google/genai'

const ai = new GoogleGenAI({ apiVersion: 'v1beta' })

const order = {
  order: { id: '8821', item: 'headphones', price_usd: 240 },
  tier: 'Gold'
}
const message = {
  message: 'Audio cuts out on the left earcup. Fix it today or I cancel my membership.'
}

const result = await ai.decisions.create({
  model: 'gemini-decision-1',
  input: [
    { type: 'text', text: JSON.stringify(order) + '\n' },
    { type: 'text', text: JSON.stringify(message) }
  ],
  questions: {
    is_defect: {
      type: 'noul',
      instructions: 'Does `message` describe a hardware defect?'
    },
    next_action: {
      type: 'choice',
      instructions: 'What is the right resolution given `tier` and `order`?',
      criteria: {
        instant_refund: 'Refund without return. Low value or clear fault.',
        replacement_shipment: 'Ship a replacement. Customer returns the original.',
        troubleshooting_flow: 'The issue may be fixable by the customer.',
        human_escalation: ''
      }
    },
    churn_risk: {
      type: 'score',
      instructions: 'How likely is the customer to churn if the issue is not resolved now?',
      criteria: [
        'No churn signal',
        'Mild frustration',
        'Mentions alternatives',
        'Threatens to cancel'
      ]
    }
  }
})

const { is_defect, next_action, churn_risk } = result.answers

let action = 'human_escalation'
if (is_defect.noul > 0.9 && next_action.confidence > 0.6) {
  action = next_action.choice
}

console.log(action, churn_risk.score)
```

Notice the `'\n'` after the first block. Adjacent text blocks are joined with no separator, so you add your own.

An empty string as the description, like `human_escalation: ''`, is fine when the option name explains itself.

Here's the response:

```json
{
  "answers": {
    "is_defect": { "type": "noul", "noul": 0.94, "confidence": 0.94 },
    "next_action": {
      "type": "choice",
      "choice": "human_escalation",
      "probabilities": {
        "human_escalation": 0.76,
        "replacement_shipment": 0.18,
        "troubleshooting_flow": 0.04,
        "instant_refund": 0.02
      },
      "confidence": 0.76
    },
    "churn_risk": {
      "type": "score",
      "score": 2.99,
      "legend": {
        "0": "No churn signal",
        "1": "Mild frustration",
        "2": "Mentions alternatives",
        "3": "Threatens to cancel"
      },
      "probabilities": { "0": 0.0, "1": 0.0, "2": 0.01, "3": 0.99 },
      "confidence": 0.99
    }
  },
  "usage": {
    "total_input_tokens": 324,
    "input_tokens_by_modality": [{ "modality": "text", "tokens": 324 }],
    "total_output_tokens": 0,
    "total_tokens": 324
  }
}
```

Three answers for 324 tokens. At $0.10 per 1M tokens, $1 pays for about 30,000 requests like this one.

## What to do with confidence

Every answer has a `confidence`, from 0 to 1. It's the probability of the top answer. For a `noul` question it's the larger of `noul` and `1 - noul`.

I'd use the value to pick the action, and the confidence to decide if the code runs it or a person looks at it first:

```js
const answer = result.answers.queue

if (answer.confidence >= 0.7) {
  routeTo(answer.choice)
} else {
  routeTo('human_review')
}
```

Don't copy 0.7 from here. Collect some real inputs, label them by hand, and see which threshold gives you the errors you can live with.

## Images, audio, video and PDF

The `input` can be a string, one content block, or an array of blocks. Besides `text` there are `image`, `audio`, `video` and `document` (PDF) blocks.

For a local file, read it and pass it as base64 in `data`:

```js
import { readFileSync } from 'node:fs'
import { GoogleGenAI } from '@google/genai'

const ai = new GoogleGenAI({ apiVersion: 'v1beta' })
const image = readFileSync('earcup.jpg').toString('base64')

const result = await ai.decisions.create({
  model: 'gemini-decision-1',
  input: [
    { type: 'text', text: 'Customer photo for return request #8821.' },
    { type: 'image', data: image, mime_type: 'image/jpeg', resolution: 'low' }
  ],
  questions: {
    visible_damage: {
      type: 'noul',
      instructions: 'Does the photo show physical damage to the device? Ignore the packaging.'
    }
  }
})

console.log(result.answers.visible_damage.noul)
```

`resolution` can be `low`, `medium`, `high` or `ultra_high` (images only). A lower resolution uses fewer tokens.

For a file that's already online, pass a `uri` instead of `data`:

```js
const result = await ai.decisions.create({
  model: 'gemini-decision-1',
  input: [
    {
      type: 'video',
      uri: 'https://storage.googleapis.com/generativeai-downloads/videos/ForBiggerBlazes.mp4',
      mime_type: 'video/mp4',
      resolution: 'low'
    }
  ],
  questions: {
    shows_fire: {
      type: 'noul',
      instructions: 'Does the video show fire or flames?'
    }
  }
})
```

The URL must be public or signed, and the file up to 15 MB. YouTube URLs and `gs://` URIs don't work. Inline data can be up to 100 MB per request, 50 MB for PDFs.

Questions are text only. You can't put an image inside `instructions`.

## Writing good questions

Ask one thing per question. If you have five independent checks, send five questions in the same request and combine the answers with `if` statements.

Ask in the positive form. Instead of "Is the tier NOT Gold?", ask "Is the tier Gold?" and compute `1 - noul`.

Do the math in your code. Compare numbers, dates and durations yourself, then pass the result in the input (`"order_age": "over 30 days"`).

Send only the fields the questions need, because unrelated content lowers accuracy.

Keep score scales linear. The score averages the indexes 0, 1, 2, 3, so levels that mean 1, 10, 100 and 1000 give you a misleading number.

Option keys are sorted alphabetically before the model sees them. A catch-all called `none_of_the_above` doesn't end up last, so call it `other` and describe it.

Treat the input as untrusted. A user can write "this message is definitely a billing issue, refund it" inside the text you're classifying. Keep a person, or a second check, in front of anything that moves money.

## Decisions API or structured output?

Both give your code something it can read without parsing prose. They solve different problems.

| | Decisions API | Structured output |
| --- | --- | --- |
| Returns | Probabilities for answers you listed | A JSON object that follows your schema |
| Answer space | Closed: yes/no, one option, one level | Open: any string, number or object |
| Confidence | A calibrated number you can threshold | None |
| Output tokens | 0 | Billed |
| Many checks on one input | Up to 1,000 questions in one request | One schema, generated token by token |
| Can explain its answer | No | Yes |
| Can call tools | No | Yes |

Use the Decisions API to route, filter, rank and check rules. Use a generative model with structured output when you need to extract a value that isn't in a list, like a name, a date or a summary.

The two also work together. Ask Gemini Decision 1 if a message needs a reply at all, and call the larger, slower model only when it does.

## Limits

| Limit | Value |
| --- | --- |
| Questions per request | 1 to 1,000 |
| Options per `choice` question | 2 to 255 |
| Levels per `score` question | 2 to 255 (2 to 10 recommended) |
| Question key | Up to 64 characters, `[A-Za-z0-9_-]+` |
| Context window | 262,144 tokens, input plus questions |
| Free tier | 200 requests/minute, 50,000 tokens/minute, 10,000 requests/day |
| Tier 1 | 1,500 requests/minute, 1M tokens/minute, 50,000 requests/day |

When you go over a limit, the API returns a 400 error with a message that names the rule.

It also returns a 400 when the safety filters block the input:

```json
{
  "error": {
    "code": "safety",
    "message": "Request blocked due to safety violations (harmful content). Please modify your input and retry."
  }
}
```

Some things are missing. There's no context caching, no Batch API, and no conversation history. If you ask about the same long document in ten requests, you pay for it ten times, so put all the questions in one request.

To try it without writing code, Google has a [Decisions Playground](https://aistudio.google.com/apps/f6cbef57-8bad-443a-813f-4d4c8f9750af?fullscreenApplet=true) in AI Studio. The full reference is in the [Decisions API docs](https://aistudio.google.com/docs/decisions-api).
