# Give an agent a typed answer

> Have an agent answer with JSON that matches a schema, read its fields in your app and in a flow, and see what happens when an answer doesn't fit.

This guide has an agent answer with JSON that matches a schema, and reads its fields in your app and in a flow.

An agent's answer is text unless you give it an `output` schema. With one, the final answer must be JSON that matches the schema. Kindgi checks it before the turn completes, and your code reads the fields.

## Before you start

- **A pack on a runtime such as `kindgi dev`,** with Kindgi 0.1's CLI.
- **A registered model,** as [Models](https://docs.kindgi.com/v0.1.10/guides/models.md) shows. `dev-echo` only repeats the message, so it can't triage a ticket.
- **For the app's half:** Node 22.12 or later, Python 3.11 or later, or Java 17 or later, with Kindgi 0.1's client, and `KINDGI_API_URL` and `KINDGI_API_TOKEN` set.

## Declare the answer

An `output` schema says what the answer must be. Ask your coding agent: "I want an agent that triages support tickets and answers with JSON my code can read." Kindgi's skills in your pack tell it how ([How it knows Kindgi](https://docs.kindgi.com/v0.1.10/start/coding-agents.md)). For reference, the agent looks like this. This guide calls it `acme.ticket-triage`:

- **TypeScript**

  `agents/ticket-triage/index.ts`

  ```ts
  import { defineAgent } from '@kindgi/sdk/define';
  import { z } from 'zod';

  export const Triage = z.object({
    category: z.enum(['billing', 'shipping', 'returns', 'other']),
    priority: z.enum(['low', 'normal', 'urgent']),
    summary: z.string(),
  });

  const defined = defineAgent({
    id: 'acme.ticket-triage',
    version: '0.1.0',
    name: 'Ticket triage',
    instructions: [
      'You triage support tickets for Acme. Reply with only a JSON object:',
      '{"category": "billing" | "shipping" | "returns" | "other",',
      ' "priority": "low" | "normal" | "urgent",',
      ' "summary": "<one sentence>"}',
    ].join('\n'),
    capabilities: [{ needs: [] }],
    tools: [],
    retrieval: [],
    guardrails: [],
    output: { schema: Triage, name: 'triage' },
  });

  if (defined.kind === 'err') throw new Error(defined.error.message);

  export default defined.value;
  ```

  `schema` takes a Zod schema or a JSON Schema object.

- **Python**

  `agents/ticket_triage.py`

  ```python
  from typing import Literal

  from pydantic import BaseModel

  from kindgi import Agent


  class Triage(BaseModel):
      category: Literal["billing", "shipping", "returns", "other"]
      priority: Literal["low", "normal", "urgent"]
      summary: str


  ticket_triage = Agent(
      id="acme.ticket-triage",
      version="0.1.0",
      name="Ticket triage",
      instructions="\n".join([
          "You triage support tickets for Acme. Reply with only a JSON object:",
          '{"category": "billing" | "shipping" | "returns" | "other",',
          ' "priority": "low" | "normal" | "urgent",',
          ' "summary": "<one sentence>"}',
      ]),
      capabilities=[{"needs": []}],
      output={"schema": Triage, "name": "triage"},
  )
  ```

  `output=Triage` alone works too. The full form also takes `name` and `maxRepairs`.

- **Java**

  `src/main/java/acme/agents/TicketTriage.java`

  ```java
  package acme.agents;

  import com.fasterxml.jackson.annotation.JsonProperty;
  import com.kindgi.pack.Agent;
  import java.util.List;
  import java.util.Map;

  public final class TicketTriage {
    public enum Category {
      @JsonProperty("billing") BILLING,
      @JsonProperty("shipping") SHIPPING,
      @JsonProperty("returns") RETURNS,
      @JsonProperty("other") OTHER
    }

    public enum Priority {
      @JsonProperty("low") LOW,
      @JsonProperty("normal") NORMAL,
      @JsonProperty("urgent") URGENT
    }

    public record Triage(Category category, Priority priority, String summary) {}

    public static final Agent AGENT = Agent.define("acme.ticket-triage")
        .version("0.1.0")
        .name("Ticket triage")
        .instructions(String.join("\n",
            "You triage support tickets for Acme. Reply with only a JSON object:",
            "{\"category\": \"billing\" | \"shipping\" | \"returns\" | \"other\",",
            " \"priority\": \"low\" | \"normal\" | \"urgent\",",
            " \"summary\": \"<one sentence>\"}"))
        .capability(Map.of("needs", List.of()))
        .output(Triage.class)
        .build();

    private TicketTriage() {}
  }
  ```

  `output(Triage.class)` derives the schema from the record. It sets no `name`, so repairs and errors call the answer `output`; for `name` or `maxRepairs`, give the schema yourself: `.set("output", Map.of("schema", <JSON Schema as a Map>, "name", "triage", "maxRepairs", 2))`.

- **Scala**

  `src/main/scala/acme/agents/TicketTriage.scala`

  ```scala
  package acme.agents

  import com.kindgi.pack.scaladsl._

  object TicketTriage {
    enum Category {
      case billing, shipping, returns, other
    }

    enum Priority {
      case low, normal, urgent
    }

    final case class Triage(category: Category, priority: Priority, summary: String)

    val agent: Agent = Agent("acme.ticket-triage")
      .version("0.1.0")
      .name("Ticket triage")
      .instructions(Seq(
        "You triage support tickets for Acme. Reply with only a JSON object:",
        """{"category": "billing" | "shipping" | "returns" | "other",""",
        """ "priority": "low" | "normal" | "urgent",""",
        """ "summary": "<one sentence>"}""").mkString("\n"))
      .capability(Map("needs" -> Seq()))
      .output[Triage]
      .build()
  }
  ```

  `output[Triage]` derives the schema from the case class. It sets no `name`, so repairs and errors call the answer `output`; for `name` or `maxRepairs`, give the schema yourself with `.set("output", Map("schema" -> …, "name" -> "triage", "maxRepairs" -> 2))`.

* **`schema`** is what the answer must match. One that doesn't compile (strict JSON Schema, draft 2020-12) is refused where the agent arrives: by `defineAgent`, when it's registered, or by a deploy (`400 deployment-validation-failed`, at `/output/schema`), not at each turn.
* **`name`** names the answer in what the model is told when it gets it wrong, and in errors. It's `output` by default.
* **`maxRepairs`** is how many times a wrong answer goes back to the model, 1 by default, as [When the answer doesn't fit](https://docs.kindgi.com/v0.1.10/guides/agents/typed-answer.md#when-the-answer-doesnt-fit) describes.
* **`capabilities: [{ needs: [] }]`** lets any registered model answer: this agent calls no tools.

The model is sent the schema from its first call. The system message ends with it, in the words a repair repeats, and a last line: give your final answer as that JSON alone. When the model is offered tools, the agent's own or memory's `remember`, that line first says to call the ones it needs. So its first answer can already fit. Describing the JSON in the instructions, as this agent does, still helps the model fill the fields well.

The schema goes with every model call, its descriptions included, so every call's prompt tokens count it. Keep it, and its descriptions, to what the answer needs.

From a local model, Qwen 2.5 7B through Ollama, with instructions that don't describe the JSON, the first call's system message ends:

```text excerpt
…"properties":{"summary":{"type":"string","description":"One sentence, for the queue"},"category":{"enum":["billing","shipping","returns","other"],"type":"string","description":"What the message is about"},"priority":{"enum":["low","normal","urgent"],"type":"string","description":"How soon it needs a person"}},"additionalProperties":false}
Give your final answer as that JSON alone.
```

and its first answer fits, in one model call:

```json
{"category":"billing","priority":"urgent","summary":"Request for refund of overcharged amount for order A-1001 by today."}
```

## Run it

A run of the agent returns the parsed answer beside the model's text:

- **TypeScript**

  ```sh
  pnpm exec kindgi runs start --agent=acme.ticket-triage --input='{"userMessage":"I was charged twice for order A-1001 and I need the money back today."}'
  ```

- **Python**

  ```sh
  uv run kindgi runs start --agent=acme.ticket-triage --input='{"userMessage":"I was charged twice for order A-1001 and I need the money back today."}'
  ```

- **Java**

  ```sh
  ./kindgiw runs start --agent=acme.ticket-triage --input='{"userMessage":"I was charged twice for order A-1001 and I need the money back today."}'
  ```

- **Scala**

  ```sh
  ./kindgiw runs start --agent=acme.ticket-triage --input='{"userMessage":"I was charged twice for order A-1001 and I need the money back today."}'
  ```

`--agent` is the agent to run, and `--input` the turn's input, with the ticket as `userMessage`. It prints the run:

````json excerpt
{
  …
  "status": "completed",
  …
  "output": {
    …
    "usage": { "steps": 1, "durationMs": 2448, "promptTokens": 88, "totalCostUsd": 0.000343, "completionTokens": 51 },
    "output": {
      "summary": "Customer was double-charged for order A-1001 and requests immediate refund.",
      "category": "billing",
      "priority": "urgent"
    },
    …
    "response": {
      "role": "agent",
      "actor": "acme.ticket-triage",
      "content": "```json\n{\n  \"category\": \"billing\",\n  \"priority\": \"urgent\",\n  \"summary\": \"Customer was double-charged for order A-1001 and requests immediate refund.\"\n}\n```",
      …
    },
    …
  }
}
````

The run's `output.output` is the parsed answer, and `output.response.content` the text the model wrote. A JSON answer in a fenced `json` code block, as here, is accepted.

## Read it in your app

Ask your coding agent: "I want my app to triage a ticket and read its category and priority." For reference, the code looks like this. Your app parses the fields with the same schema:

- **TypeScript**

  `read-triage.ts`

  ```ts
  import { createClient } from '@kindgi/sdk/client';

  import { Triage } from './agents/ticket-triage/index.js';

  const kindgi = createClient(); // KINDGI_API_URL, KINDGI_API_TOKEN

  const run = await kindgi.runs.start({
    agent: 'acme.ticket-triage',
    input: { userMessage: 'I was charged twice for order A-1001 and I need the money back today.' },
  });
  const triage = Triage.parse((run.output as { output: unknown }).output);
  console.log(triage.category, triage.priority); // billing urgent
  ```

- **Python**

  `read_triage.py`

  ```python
  from kindgi.client import Kindgi

  from agents.ticket_triage import Triage

  kindgi = Kindgi()  # KINDGI_API_URL, KINDGI_API_TOKEN

  run = kindgi.runs.start(
      agent="acme.ticket-triage",
      input={"userMessage": "I was charged twice for order A-1001 and I need the money back today."},
  )
  if run.output is None:
      raise SystemExit(f"The run has no output: it's {run.status}.")
  triage = Triage.model_validate(run.output["output"])
  print(triage.category, triage.priority)  # billing urgent
  ```

- **Java**

  `src/main/java/com/acme/app/ReadTriage.java`

  ```java
  package com.acme.app;

  import com.kindgi.client.Kindgi;
  import com.kindgi.client.models.Run;
  import com.kindgi.client.models.StartRunBody;
  import java.util.Map;

  public final class ReadTriage {
    public static void main(String[] args) {
      Kindgi kindgi = Kindgi.create(); // KINDGI_API_URL, KINDGI_API_TOKEN

      Run run = kindgi.runs().start(StartRunBody.WithAgent.builder()
          .agent("acme.ticket-triage")
          .input(Map.of("userMessage", "I was charged twice for order A-1001 and I need the money back today."))
          .build());
      Map<?, ?> triage = (Map<?, ?>) ((Map<?, ?>) run.output()).get("output");
      System.out.println(triage.get("category") + " " + triage.get("priority")); // billing urgent
    }
  }
  ```

  The client's JSON reader doesn't see your types' Jackson annotations, so read the fields from the answer's map.

- **Scala**

  `src/main/scala/acme/app/ReadTriage.scala`

  ```scala
  package acme.app

  import acme.agents.TicketTriage.Triage
  import com.kindgi.client.{Kindgi, KindgiJson}
  import com.kindgi.client.models.StartRunBody
  import scala.jdk.CollectionConverters._

  object ReadTriage {
    def main(args: Array[String]): Unit = {
      val kindgi = Kindgi.create() // KINDGI_API_URL, KINDGI_API_TOKEN

      val run = kindgi.runs().start(StartRunBody.WithAgent.builder()
        .agent("acme.ticket-triage")
        .input(Map[String, AnyRef](
          "userMessage" -> "I was charged twice for order A-1001 and I need the money back today.").asJava)
        .build())
      val answer = run.output.asInstanceOf[java.util.Map[String, AnyRef]].get("output")
      val triage = KindgiJson.read(KindgiJson.write(answer), classOf[Triage])
      println(s"${triage.category} ${triage.priority}") // billing urgent
    }
  }
  ```

## When the answer doesn't fit

An answer that isn't JSON, or doesn't match the schema, goes back to the model with a message that lists what's wrong and gives the schema:

```text excerpt
[kindgi:output-repair]
Your answer must be the triage as JSON matching this JSON Schema, and nothing else:
{"$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"properties":{"category":{"enum":["billing","shipping","returns","other"],"type":"string"},…},"required":["category","priority","summary"],"type":"object"}
It did not fit:
- the answer is not valid JSON (Unexpected token 'I', "I understa"... is not valid JSON)
Reply again with only the corrected triage JSON.
```

Here an older runtime, which didn't send the schema with the first call, ran a version of the agent whose instructions didn't describe the JSON: its first answer was prose. The repair worked, and the turn took two steps instead of one. Each repair is a model call: it counts as a step against the agent's [budget](https://docs.kindgi.com/v0.1.10/guides/agents/budgets.md), and it costs.

When the repairs run out, the turn fails with `output-schema-violation`. `maxRepairs` sets how many, 1 by default. With `maxRepairs: 0`, an answer whose `priority` is `"high"` fails at once:

```text
Error [output-schema-violation]: The agent's answer does not match its triage schema after 0 repairs: /priority must be equal to one of the allowed values
```

A Zod object refuses fields it doesn't name: its schema has `"additionalProperties": false`, as the repair message shows. The schema of the pydantic model doesn't.

## In a flow

A flow reads a typed answer at `nodeOutputs.<step>.output.<field>`. Ask your coding agent: "I want a flow that triages a ticket and returns its category and priority." For reference, the flow looks like this:

- **TypeScript**

  `flows/triage-ticket/index.ts`

  ```ts
  import { defineFlow } from '@kindgi/sdk/define';

  const defined = defineFlow({
    id: 'acme.triage-ticket',
    version: '0.1.0',
    name: 'Triage a ticket',
    nodes: [
      {
        id: 'triage',
        kind: 'agent',
        ref: 'acme.ticket-triage',
        inputMapping: { ticket: { path: 'runInput.ticket' } },
      },
    ],
    edges: [
      { id: 'e-start', from: '$start', to: 'triage' },
      { id: 'e-end', from: 'triage', to: '$end' },
    ],
    output: {
      mapping: {
        category: { path: 'nodeOutputs.triage.output.category' },
        priority: { path: 'nodeOutputs.triage.output.priority' },
      },
    },
  });

  if (defined.kind === 'err') throw new Error(defined.error.message);

  export default defined.value;
  ```

- **Python**

  `flows/triage_ticket.py`

  ```python
  from kindgi import Flow

  from ..agents.ticket_triage import ticket_triage

  triage_ticket = Flow(
      id="acme.triage-ticket",
      version="0.1.0",
      name="Triage a ticket",
      nodes=[
          {
              "id": "triage",
              "kind": "agent",
              "ref": ticket_triage,
              "inputMapping": {"ticket": {"path": "runInput.ticket"}},
          },
      ],
      edges=[
          {"id": "e-start", "from": "$start", "to": "triage"},
          {"id": "e-end", "from": "triage", "to": "$end"},
      ],
      output={
          "mapping": {
              "category": {"path": "nodeOutputs.triage.output.category"},
              "priority": {"path": "nodeOutputs.triage.output.priority"},
          },
      },
  )
  ```

- **Java**

  `src/main/java/acme/flows/TriageTicket.java`

  ```java
  package acme.flows;

  import acme.agents.TicketTriage;
  import com.kindgi.pack.Flow;
  import java.util.Map;

  public final class TriageTicket {
    public static final Flow FLOW = Flow.define("acme.triage-ticket")
        .version("0.1.0")
        .set("name", "Triage a ticket")
        .node(Map.of(
            "id", "triage",
            "kind", "agent",
            "ref", TicketTriage.AGENT,
            "inputMapping", Map.of("ticket", Map.of("path", "runInput.ticket"))))
        .edge("e-start", "$start", "triage")
        .edge("e-end", "triage", "$end")
        .set("output", Map.of("mapping", Map.of(
            "category", Map.of("path", "nodeOutputs.triage.output.category"),
            "priority", Map.of("path", "nodeOutputs.triage.output.priority"))))
        .build();

    private TriageTicket() {}
  }
  ```

- **Scala**

  `src/main/scala/acme/flows/TriageTicket.scala`

  ```scala
  package acme.flows

  import acme.agents.TicketTriage
  import com.kindgi.pack.scaladsl._

  object TriageTicket {
    val flow: Flow = Flow("acme.triage-ticket")
      .version("0.1.0")
      .set("name", "Triage a ticket")
      .node(Map(
        "id" -> "triage",
        "kind" -> "agent",
        "ref" -> TicketTriage.agent,
        "inputMapping" -> Map("ticket" -> Map("path" -> "runInput.ticket"))))
      .edge("e-start", "$start", "triage")
      .edge("e-end", "triage", "$end")
      .set("output", Map("mapping" -> Map(
        "category" -> Map("path" -> "nodeOutputs.triage.output.category"),
        "priority" -> Map("path" -> "nodeOutputs.triage.output.priority"))))
      .build()
  }
  ```

Run the flow:

- **TypeScript**

  ```sh
  pnpm exec kindgi runs start --flow=acme.triage-ticket --input='{"ticket":"I was charged twice for order A-1001 and I need the money back today."}'
  ```

- **Python**

  ```sh
  uv run kindgi runs start --flow=acme.triage-ticket --input='{"ticket":"I was charged twice for order A-1001 and I need the money back today."}'
  ```

- **Java**

  ```sh
  ./kindgiw runs start --flow=acme.triage-ticket --input='{"ticket":"I was charged twice for order A-1001 and I need the money back today."}'
  ```

- **Scala**

  ```sh
  ./kindgiw runs start --flow=acme.triage-ticket --input='{"ticket":"I was charged twice for order A-1001 and I need the money back today."}'
  ```

`--flow` is the flow, and `--input` its input, with the ticket. The run's output holds the two fields the flow mapped:

```json excerpt
{
  …
  "flowId": "acme.triage-ticket",
  …
  "status": "completed",
  …
  "output": {
    "category": "billing",
    "priority": "urgent"
  }
}
```

## Dry runs

A dry run, with `--dry-run`, skips the model call, and with it the check. The turn completes with no `output`, and its answer is `[dry-run: model call skipped]`.

## Next

- [Give an agent its input](https://docs.kindgi.com/v0.1.10/guides/agents/give-an-agent-input.md)
- [Pass data between steps](https://docs.kindgi.com/v0.1.10/guides/flows/pass-data-between-steps.md)
- [Set an agent's budget](https://docs.kindgi.com/v0.1.10/guides/agents/budgets.md)
