> ## Documentation Index
> Fetch the complete documentation index at: https://docs.moonbounce.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Call a decision from an agent

> Where a decision sits in an agent you ship, such as a customer service or refund agent, and how the agent's loop calls it.

This page is about an agent you run in production. To connect the coding agent that builds and improves your decisions, see [Connect your agent](https://docs.moonbounce.ai/connect-your-agent.md).

An agent doesn't load a decision or carry its questions in its prompt. At the step where the decision applies, your code makes one HTTP call, `POST /v1/evaluate`, with what the agent knows so far. Moonbounce asks the questions on its own model and returns an outcome, and your code does what that outcome means. There is no SDK to install.

## Where each part lives

| Part | Where it lives |
| - | - |
| The decision: questions, aggregations, cut points | One file in your agent's repository, `moonbounce/<decision_id>.json`, deployed with the agent |
| The key | A secret in the agent's environment, with `decisions:evaluate` |
| What each call sends | `state` for the questions to read and `attribute_values` for the rules, built from the customer's messages and your own records |
| The record of what ran | Moonbounce: every evaluation stores the version it ran, the answers and the outcome |

The agent's own model never sees the questions and can run on anything, because they're answered on the decision's [model setting](https://docs.moonbounce.ai/learn/models-and-environments.md).

## Two ways to call it

**A gate in your code.** Your tool checks the decision before it does the thing. The agent chooses when to try, and the decision chooses whether it happens. Use this whenever the outcome allows something that costs money or can't be undone, because a model can skip a tool but it can't skip your code.

**A tool the model can call.** You give the model a tool such as `check_refund_policy`, return the outcome, and let it continue. Use this when the outcome only informs the conversation: routing, tone, what to ask next.

## A refund agent

The agent has a tool that issues a refund. The handler asks the decision first. This decision's questions read `state.message` and `state.order`, and its outcomes are `grant`, `escalate` and `deny`.

```ts theme={null}
import { createHash } from "node:crypto";
import decision from "./moonbounce/refund-exception.json" with { type: "json" };

async function decide(ticketId: string, message: string, order: unknown) {
  const body = JSON.stringify({
    state: { message, order },
    ...decision.structure,
    moonbounce: { decision_id: decision.decision_id, item_id: ticketId },
  });
  const res = await fetch("https://api.moonbounce.ai/v1/evaluate", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.MOONBOUNCE_API_KEY}`,
      "Content-Type": "application/json",
      // A retry of this exact request within 24 hours returns the first evaluation instead of running a second.
      "Idempotency-Key": `evaluate:${createHash("sha256").update(body).digest("hex")}`,
    },
    body,
    signal: AbortSignal.timeout(12_000),
  });
  if (!res.ok) throw new Error(`evaluate failed: ${res.status}`);
  return res.json();
}

// The handler for the agent's issue_refund tool, which takes no arguments. Your loop passes the ticket
// this conversation belongs to. What the handler returns goes back to the model.
async function issueRefund(ticketId: string) {
  // From your own records, never from the model: a customer can steer what the model passes to a tool.
  const ticket = await loadTicket(ticketId);

  let verdict;
  try {
    verdict = await decide(ticket.id, ticket.conversation, ticket.order);
  } catch {
    return await escalate(ticket, "The refund check failed.");
  }

  // Partial evidence or no answer goes to a person, as does an outcome this code doesn't know.
  if (verdict.status !== "decided" || verdict.missing?.length) {
    return await escalate(ticket, "The refund check wasn't sure.");
  }
  // refund must be safe to call twice for one ticket: the model can call this tool again.
  if (verdict.outcome === "grant") return await refund(ticket);
  if (verdict.outcome === "deny") return "Not refunded. Explain the refund policy to the customer.";
  return await escalate(ticket, "This refund needs a person.");
}
```

`loadTicket`, `refund` and `escalate` are your own code. Your agent loop calls `issueRefund` when the model asks for the tool. The `Idempotency-Key` protects the evaluation, not the refund: a model can call the tool twice and get `grant` both times, so make the refund itself idempotent on the ticket id.

## What to send

* **`state`** is what the questions read. Send the customer's messages from your stored transcript as one of its values, and only the part the questions need: each question's `reads` lists its paths.
* **`attribute_values`** carries facts your code loaded from your records, when the decision declares attributes it doesn't derive with `from`. Facts the code already knows shouldn't be left for a model to judge ([Attributes](https://docs.moonbounce.ai/learn/decisions.md#attributes-values-your-app-supplies)).
* **`item_id`** is your own id for the case, such as the ticket. Feedback and reviews join up on it, so use the same one for the whole conversation.

**Only the customer's words should come from the conversation.** Load anything the decision treats as a fact, such as an order or a refund history, from your own records and never from the model's tool arguments. Otherwise a customer can steer the model into passing an order that earns a `grant`. The customer's message can still try to sway the questions that read it, so where money depends on a fact, make it a number, boolean or enum attribute that a rule checks. The example keeps the order in `state` to stay short.

Keep the response's `id` beside the action you took. Feedback on one specific result names it ([Feedback](https://docs.moonbounce.ai/learn/feedback.md)).

## In the loop

* **Plan for a slow answer.** A call has a 10-second budget, so give it a timeout a little longer than that, as in the example, and decide what the agent says while it waits. [Evaluate content](https://docs.moonbounce.ai/learn/evaluations.md) lists which errors to retry.
* **Use one call per decision.** If the agent needs two decisions, make two calls and chain them in your code.
* **Turn the outcome into words.** The outcome is a name such as `grant`. Your handler decides what the model reads next, as in the example.

## Changing the decision

Edit the one file and merge it. When the agent deploys, its next call runs the new structure, and the response's `version` says so. There is no pointer to move. Your git history holds every earlier version, so a revert and a deploy restores an earlier structure.

The model a decision runs on isn't in the file. It's a setting kept for each environment, and the first call in an environment without one stores the default model. To run on another model, register the decision and set its model before the agent's first call, which needs a key with `decisions:develop` ([Models and environments](https://docs.moonbounce.ai/learn/models-and-environments.md)). A rollback doesn't change it.

Sending the structure on every call means a new version needs no separate registration step. To keep request bodies small, you can register the structure ahead of traffic, which needs a key with `decisions:develop`, and send `moonbounce.structure_hash` instead. An unregistered hash is a 404, so register before you deploy ([Versions](https://docs.moonbounce.ai/learn/versions.md#registering-a-version)).

## Why not the MCP server

The MCP server at `/mcp` is for the coding agent that builds and improves decisions, and a connection lists every tool, including the ones that change a decision. An agent in production needs only the one call above, over plain HTTP.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.