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

> ## Agent Instructions
> H Platform has four products: the Agents API (managed computer-use agents, base URL https://agp.eu.hcompany.ai/api/v2 or https://agp.hcompany.ai/api/v2 for the US), the Models API (OpenAI-compatible Holo vision-language models at https://api.hcompany.ai/v1), HoloDesktop CLI (Holo on the user's own desktop), and HoloTab (a free no-code Chrome extension that runs Holo in the user's browser, with recordable routines and schedules).
> Authenticate with a bearer API key from the HAI_API_KEY environment variable. SDKs: `pip install hai-agents` (Python, `from hai_agents import Client`) and `npm install hai-agents` (TypeScript, `import { HaiAgentsClient } from "hai-agents"`). CLI: `hai`.
> Agents do work in a browser or on a desktop; describe the task as an imperative instruction. To run a task quickly, prefer the pre-built agent `h/web-surfer-flash`. Read results from the session's `latest_answer` after it reaches a terminal status.
> Sessions are the unit of work; wait for a terminal status (completed, failed, timed_out, interrupted) before reading the answer. Use webhooks or the `changes` long-poll endpoint to follow progress.

# Structured outputs

> The Holo3 and Holo3.1 agent loop with one constrained JSON object per step, and the mistakes specific to this format.

export const ChatStrip = ({rows, repeatFrom = 1}) => {
  const Body = ({text}) => text.split(/(`[^`]+`)/).map((part, i) => part.startsWith("`") ? <code key={i} className="rounded bg-zinc-100 px-1 py-0.5 font-mono text-[12px] text-zinc-800 dark:bg-zinc-800 dark:text-zinc-100">{part.slice(1, -1)}</code> : <span key={i}>{part}</span>);
  const Row = ({role, body}) => <div className="flex items-center gap-3">
      <span className={`${role === "system" ? "w-[84px] shrink-0 rounded-md px-2 py-0.5 text-center font-mono text-xs bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-900" : role === "user" ? "w-[84px] shrink-0 rounded-md px-2 py-0.5 text-center font-mono text-xs bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-200" : role === "assistant" ? "w-[84px] shrink-0 rounded-md px-2 py-0.5 text-center font-mono text-xs border border-zinc-300 text-zinc-700 dark:border-zinc-600 dark:text-zinc-200" : "w-[84px] shrink-0 rounded-md px-2 py-0.5 text-center font-mono text-xs border border-dashed border-zinc-400 text-zinc-600 dark:border-zinc-500 dark:text-zinc-300"}`}>{role}</span>
      <span className="text-sm leading-5 text-zinc-700 dark:text-zinc-300"><Body text={body} /></span>
    </div>;
  return <div className="not-prose my-6 overflow-x-auto">
      <div className="flex min-w-[520px] flex-col gap-2 rounded-xl border border-zinc-200 bg-white p-4 dark:border-zinc-800 dark:bg-zinc-950">
        {rows.slice(0, repeatFrom).map((r, i) => <Row key={i} {...r} />)}
        <div className="my-1 flex items-center gap-3 text-xs text-zinc-500 dark:text-zinc-400">
          <span className="h-px flex-1 bg-zinc-200 dark:bg-zinc-800" />
          <span className="whitespace-nowrap">repeats every step</span>
          <span className="h-px flex-1 bg-zinc-200 dark:bg-zinc-800" />
        </div>
        {rows.slice(repeatFrom).map((r, i) => <Row key={i} {...r} />)}
      </div>
    </div>;
};

export const Notice = ({kind = "note", title, children}) => {
  const kinds = {
    warning: {
      label: "User notice",
      icon: <>
          <path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" />
          <path d="M12 9v4" />
          <path d="M12 17h.01" />
        </>
    },
    gotcha: {
      label: "Gotcha",
      icon: <>
          <circle cx="12" cy="12" r="10" />
          <path d="M12 16v-4" />
          <path d="M12 8h.01" />
        </>
    },
    note: {
      label: "Note",
      icon: <>
          <circle cx="12" cy="12" r="10" />
          <path d="M12 16v-4" />
          <path d="M12 8h.01" />
        </>
    }
  };
  const k = kinds[kind];
  return <div className="notice my-6 rounded-xl border border-zinc-200 bg-white p-5 dark:border-zinc-800 dark:bg-zinc-950">
      <div className={`${kind === "warning" ? "not-prose flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-red-400/80 dark:text-red-400/70" : kind === "gotcha" ? "not-prose flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-amber-500/80 dark:text-amber-400/70" : "not-prose flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-zinc-400 dark:text-zinc-500"}`}>
        <svg className="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
          {k.icon}
        </svg>
        {k.label}
      </div>
      {title && <div className="not-prose mt-2 text-base font-semibold text-zinc-900 dark:text-zinc-100">{title}</div>}
      <div className="notice-body mt-3 text-sm leading-6 text-zinc-700 dark:text-zinc-300">{children}</div>
    </div>;
};

The model is constrained, at the decoding level, to emit a single JSON object matching a schema you provide. Tool calls are fields inside that object, so output is always valid JSON. Works on Holo3.1 (`holo3-1-35b-a3b`) and Holo3 (`holo3-122b-a10b`).

<Notice kind="note" title="On Holo3.1, prefer function calling">
  Use [function calling](/models-api/build-an-agent/function-calling) on Holo3.1 unless you need a constrained JSON object per step.
</Notice>

Read [Core concepts](/models-api/build-an-agent/core-concepts) first: reasoning, coordinates, observations, and the `trim_to_last_n_images` helper used below.

## Output JSON

Each step, the model emits one object with three fields:

```json theme={"system"}
{
  "note": "Submit succeeded; receipt URL is /orders/8421.",
  "thought": "Recording the receipt URL before navigating away.",
  "tool_call": {
    "tool_name": "click",
    "element": "Continue button at the bottom right",
    "x": 932,
    "y": 880
  }
}
```

<ResponseField name="note" type="string | null">
  The model's durable memory: anything from the current screen that future steps will need (URLs, IDs, intermediate answers). `null` when nothing new is worth recording.
</ResponseField>

<ResponseField name="thought" type="string" required>
  A one-line plan for the next action.
</ResponseField>

<ResponseField name="tool_call" type="object" required>
  One variant of your tool union. Flat: `tool_name` is a sibling of the arguments, not nested in an `args` object.
</ResponseField>

## Constrain output to a tool union

Define each tool as a Pydantic model with a `Literal[tool_name]` field, then use their union as the response schema. The server's constrained decoder ensures the model emits exactly one variant, and `tool_name` is the tag you dispatch on at execution time. The example ships three tools (click, write, answer) for illustration. Real agents register a wider toolbox following the same pattern.

<CodeGroup>
  ```python Python expandable theme={"system"}
  from typing import Literal
  from pydantic import BaseModel, Field

  class ClickArgs(BaseModel):
      """Click at (x, y) coordinates"""
      tool_name: Literal["click"]
      element: str = Field(description="Detailed description of the target UI element to click on")
      x: int = Field(description="X coordinate as integer in [0, 1000]")
      y: int = Field(description="Y coordinate as integer in [0, 1000]")

  class WriteArgs(BaseModel):
      """Type text into the currently focused element without clicking first"""
      tool_name: Literal["write"]
      content: str = Field(description="Content to write")
      press_enter: bool = Field(default=False, description="Whether to press Enter after typing")

  class AnswerArgs(BaseModel):
      """Provide a final answer"""
      tool_name: Literal["answer"]
      content: str = Field(description="The answer content")

  class Step(BaseModel):
      note: str | None = Field(default=None, description="Task-relevant information from the previous observation. Empty if nothing new.")
      thought: str = Field(description="Reasoning about next steps")
      tool_call: ClickArgs | WriteArgs | AnswerArgs
  ```

  ```typescript TypeScript expandable theme={"system"}
  // JSON Schema for the Step union; tool_name is the discriminator you dispatch on
  const schema = {
    type: "object",
    properties: {
      note: {
        type: ["string", "null"],
        description: "Task-relevant information from the previous observation. Empty if nothing new.",
      },
      thought: { type: "string", description: "Reasoning about next steps" },
      tool_call: {
        oneOf: [
          {
            type: "object",
            description: "Click at (x, y) coordinates",
            properties: {
              tool_name: { const: "click" },
              element: { type: "string", description: "Detailed description of the target UI element to click on" },
              x: { type: "integer", description: "X coordinate as integer in [0, 1000]" },
              y: { type: "integer", description: "Y coordinate as integer in [0, 1000]" },
            },
            required: ["tool_name", "element", "x", "y"],
          },
          {
            type: "object",
            description: "Type text into the currently focused element without clicking first",
            properties: {
              tool_name: { const: "write" },
              content: { type: "string", description: "Content to write" },
              press_enter: { type: "boolean", description: "Whether to press Enter after typing" },
            },
            required: ["tool_name", "content"],
          },
          {
            type: "object",
            description: "Provide a final answer",
            properties: {
              tool_name: { const: "answer" },
              content: { type: "string", description: "The answer content" },
            },
            required: ["tool_name", "content"],
          },
        ],
      },
    },
    required: ["thought", "tool_call"],
  };
  ```
</CodeGroup>

Embed the same schema inside the system prompt under an `<output_format>` block (shown in [the loop](#a-complete-loop) below). The model was trained with the schema visible in both the prompt and `structured_outputs`, and dropping either copy noticeably hurts reliability.

<Notice kind="gotcha" title="Structured outputs, not OpenAI tools">
  Use `extra_body={"structured_outputs": {"json": ...}}`, not `tools=[...]` / `tool_choice=...`. The action arrives in `content`, not in a `tool_calls` array.
</Notice>

## Chat layout

Tool results come back as `user` messages wrapped in `<tool_output tool="...">`, not as OpenAI `tool`-role messages:

<ChatStrip
  rows={[
{ role: "system", body: "your prompt, then the appended `<output_format>` schema block" },
{ role: "user", body: "`<observation>` screenshot and/or text `</observation>`" },
{ role: "assistant", body: "`{ note, thought, tool_call }`" },
{ role: "user", body: "`<tool_output tool=\"click\">` result `</tool_output>`" },
]}
/>

## A complete loop

Pass the schema to `structured_outputs` and parse `content` back into your models. Because `tool_name` is a discriminator, the parsed `tool_call` narrows to exactly one variant, which is what you dispatch on. Highlighted lines are the ones specific to structured outputs; everything else is the shared loop from [Core concepts](/models-api/build-an-agent/core-concepts). Plug in your own `screenshot()` (browser, OS, emulator) and `execute(...)` dispatcher.

<CodeGroup>
  ````python Python highlight={3-4,22,24-25,31-34} theme={"system"}
  import json, base64

  schema = Step.model_json_schema()
  system = render_prompt(tools=...) + f"\n\n<output_format>\n```json\n{json.dumps(schema)}\n```\n</output_format>"

  messages = [{"role": "system", "content": system}]

  for _ in range(MAX_STEPS):
      image_bytes = screenshot()
      b64 = base64.b64encode(image_bytes).decode()
      messages.append({"role": "user", "content": [
          {"type": "text", "text": "<observation>\n"},
          {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}},
          {"type": "text", "text": "\n</observation>"},
      ]})
      trim_to_last_n_images(messages, n=3)

      resp = client.chat.completions.create(
          model=MODEL_NAME,
          messages=messages,
          temperature=0.8,
          extra_body={"structured_outputs": {"json": schema}},
      )
      step = Step.model_validate_json(resp.choices[0].message.content)
      messages.append({"role": "assistant", "content": step.model_dump_json()})

      if step.tool_call.tool_name == "answer":
          return step.tool_call.content

      result = execute(step.tool_call)
      messages.append({
          "role": "user",
          "content": f'<tool_output tool="{step.tool_call.tool_name}">\n{result}\n</tool_output>',
      })
  ````

  ```typescript TypeScript highlight={1-3,23,25-26,33-36} theme={"system"}
  const system =
    renderPrompt() +
    `\n\n<output_format>\n\`\`\`json\n${JSON.stringify(schema)}\n\`\`\`\n</output_format>`;

  const messages: any[] = [{ role: "system", content: system }];

  for (let i = 0; i < MAX_STEPS; i++) {
    const b64 = (await screenshot()).toString("base64");
    messages.push({
      role: "user",
      content: [
        { type: "text", text: "<observation>\n" },
        { type: "image_url", image_url: { url: `data:image/png;base64,${b64}` } },
        { type: "text", text: "\n</observation>" },
      ],
    });
    trimToLastNImages(messages, 3);

    const resp = await client.chat.completions.create({
      model: MODEL_NAME,
      messages,
      temperature: 0.8,
      ...({ structured_outputs: { json: schema } } as any),
    });
    const step = JSON.parse(resp.choices[0].message.content!);
    messages.push({ role: "assistant", content: JSON.stringify(step) });

    if (step.tool_call.tool_name === "answer") {
      return step.tool_call.content;
    }

    const result = await execute(step.tool_call);
    messages.push({
      role: "user",
      content: `<tool_output tool="${step.tool_call.tool_name}">\n${result}\n</tool_output>`,
    });
  }
  ```
</CodeGroup>

## Format-specific pitfalls

| Symptom                      | Likely cause                                                                                              |
| :--------------------------- | :-------------------------------------------------------------------------------------------------------- |
| Model returns free-form text | `extra_body.structured_outputs.json` is missing, or the schema lacks `Literal[tool_name]` discrimination  |
| Tool result has no effect    | Sent as a `tool`-role message instead of a `user` message with a `<tool_output>` wrapper                  |
| Model forgets earlier facts  | `note` left empty; it is the only memory carried between turns                                            |
| Reliability drops            | Schema present in `structured_outputs` but missing from the `<output_format>` prompt block, or vice versa |

## Next steps

<CardGroup cols={2}>
  <Card title="Element localization" icon="crosshairs" href="/models-api/element-localization">
    Get click coordinates from a screenshot.
  </Card>

  <Card title="API reference" icon="code" href="/models-api/api-reference">
    Endpoint, models, parameters, and limits.
  </Card>
</CardGroup>
