> ## 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.

# Core concepts

> The harness conventions Holo is trained on: output format, answer tool, reasoning, coordinates, observations, and sampling.

export const CoordinateGrid = () => {
  const X0 = 60;
  const Y0 = 28;
  const W = 300;
  const H = 188;
  const px = X0 + 932 / 1000 * W;
  const py = Y0 + 880 / 1000 * H;
  const mono = {
    fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
    fontSize: 11
  };
  return <div className="not-prose my-6 flex justify-center">
      <svg width="420" height="252" viewBox="0 0 420 252" className="max-w-full">
        <g className="text-zinc-50 dark:text-zinc-900" fill="currentColor">
          <rect x={X0} y={Y0} width={W} height={H} rx="4" />
        </g>
        <g className="text-zinc-200 dark:text-zinc-800" stroke="currentColor" strokeWidth="1" fill="none">
          {[0.25, 0.5, 0.75].map(f => <line key={`v${f}`} x1={X0 + f * W} y1={Y0} x2={X0 + f * W} y2={Y0 + H} />)}
          {[0.25, 0.5, 0.75].map(f => <line key={`h${f}`} x1={X0} y1={Y0 + f * H} x2={X0 + W} y2={Y0 + f * H} />)}
        </g>
        <rect x={X0} y={Y0} width={W} height={H} rx="4" fill="none" stroke="currentColor" strokeWidth="1" className="text-zinc-300 dark:text-zinc-700" />

        <g className="text-zinc-900 dark:text-zinc-100" stroke="currentColor" strokeWidth="1.25" strokeDasharray="4 4" fill="none">
          <line x1={px} y1={Y0} x2={px} y2={py} />
          <line x1={X0} y1={py} x2={px} y2={py} />
        </g>
        <circle cx={px} cy={py} r="5" className="text-zinc-900 dark:text-zinc-100" fill="currentColor" />

        <g className="text-zinc-500 dark:text-zinc-400" fill="currentColor" style={mono}>
          <text x={X0} y={Y0 - 10}>(0, 0)</text>
          <text x={px} y={Y0 - 10} textAnchor="middle">x = 932</text>
          <text x={X0 - 8} y={py + 4} textAnchor="end">y = 880</text>
          <text x={X0 + W} y={Y0 + H + 18} textAnchor="end">(1000, 1000)</text>
        </g>
      </svg>
    </div>;
};

Holo is trained as a multi-step agent inside a specific harness. Bring these conventions into yours: skip one and quality drops. Client setup is in the [Quickstart](/models-api/quickstart).

## Pick an output format

| Agent-loop format | Holo4 (`holo4-35b-a3b`, `holo4-27b`) | Holo3.1 (`holo3-1-35b-a3b`) | Holo3 (`holo3-122b-a10b`) |
| :- | :- | :- | :- |
| [Function calling](/models-api/build-an-agent/function-calling) | <Icon icon="check" /> Use this | <Icon icon="check" /> | <Icon icon="xmark" /> |
| [Structured outputs](/models-api/build-an-agent/structured-outputs) | <Icon icon="xmark" /> Not for the loop | <Icon icon="check" /> | <Icon icon="check" /> Use this |

Pick one and stay in it. The formats differ only in how you declare tools and read the output. The rest of this page applies to both.

Single-shot JSON through `structured_outputs` works on every model, Holo4 included: [element localization](/models-api/element-localization) uses it.

## Answer tool

Holo ends a task by calling a tool named `answer`, with its report in the `content` argument. Register it with your action tools, in both formats, and stop the loop when it is called. A step with no tool call is not a stop: append the message and keep looping.

## Reasoning

Each call returns a thinking trace in `message.reasoning` and the action in `content` (or `tool_calls`). Holo is trained to plan before each step, so keep thinking on in agent loops:

<CodeGroup>
  ```python Python theme={"system"}
  extra_body={"chat_template_kwargs": {"enable_thinking": True}}
  ```

  ```typescript TypeScript theme={"system"}
  // H-specific fields are passed through in the request body
  ...({ chat_template_kwargs: { enable_thinking: true } } as any)
  ```
</CodeGroup>

| Model | `reasoning_effort` |
| :- | :- |
| `holo4-27b` | `low`, `medium`, or `xhigh` (default when omitted). `high` returns `400`. `medium` suits agent loops. |
| `holo4-35b-a3b`, `holo3-1-35b-a3b` | No effort levels. |

On Holo4, `reasoning_effort: "none"` turns thinking off. The Agents API spelling `disabled` returns `400` here. All parameters: [Chat completions](/models-api/chat-completions).

The [Qwen 3.5 chat template](https://huggingface.co/Qwen/Qwen3.5-35B-A3B/blob/main/chat_template.jinja) Holo inherits drops past reasoning between turns, so anything the model must remember goes through `content`. When you append the assistant message, push only the parsed output, never the reasoning.

## Coordinates in `[0, 1000]`

Send a screenshot at any size. Holo returns integer coordinates in `[0, 1000]`, normalized to that image, origin top-left. Scale back with its dimensions:

<CoordinateGrid />

<CodeGroup>
  ```python Python theme={"system"}
  abs_x = int((x / 1000) * screenshot.width)
  abs_y = int((y / 1000) * screenshot.height)
  ```

  ```typescript TypeScript theme={"system"}
  const absX = Math.round((x / 1000) * screenshot.width);
  const absY = Math.round((y / 1000) * screenshot.height);
  ```
</CodeGroup>

Send and scale against the same image bytes: any resize, crop, or DPI mismatch misclicks. Pick one pixel unit (CSS or device) and keep it end to end.

## Observations

An observation is what the agent sees after it acts. Agents without a screen ([code](/models-api/cookbooks/code-agent), [MCP](/models-api/cookbooks/mcp-agent)) need nothing extra: the task is the first `user` message and each tool result goes back as a tool result. The rest of this section is for screen agents.

Each step, send the current screenshot as a `user` message wrapped in `<observation>` tags:

<CodeGroup>
  ```python Python theme={"system"}
  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>"},
  ]})
  ```

  ```typescript TypeScript theme={"system"}
  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>" },
    ],
  });
  ```
</CodeGroup>

### Image budget

Keep at most the last 3 screenshots in context: more degrades accuracy. Replace older ones with a text placeholder and keep their `<observation>` wrapper. Call this helper after appending each observation. The loop guides and cookbooks use it.

<CodeGroup>
  ```python Python expandable theme={"system"}
  def trim_to_last_n_images(messages, n=3):
      seen = 0
      for msg in reversed(messages):
          if msg["role"] != "user" or not isinstance(msg["content"], list):
              continue
          for chunk in msg["content"]:
              if chunk.get("type") != "image_url":
                  continue
              seen += 1
              if seen > n:
                  chunk["type"] = "text"
                  chunk["text"] = "[screenshot evicted]"
                  chunk.pop("image_url", None)
  ```

  ```typescript TypeScript expandable theme={"system"}
  function trimToLastNImages(messages: any[], n = 3) {
    let seen = 0;
    for (let i = messages.length - 1; i >= 0; i--) {
      const msg = messages[i];
      if (msg.role !== "user" || !Array.isArray(msg.content)) continue;
      for (const chunk of msg.content) {
        if (chunk.type !== "image_url") continue;
        seen += 1;
        if (seen > n) {
          chunk.type = "text";
          chunk.text = "[screenshot evicted]";
          delete chunk.image_url;
        }
      }
    }
  }
  ```
</CodeGroup>

## Sampling

| Task | Thinking | `temperature` |
| :- | :- | :- |
| Agent loops | On | `0.8` |
| Single calls: [element localization](/models-api/element-localization), [document OCR](/models-api/document-ocr) | Off | `0.0` |

## Common pitfalls

| Symptom | Likely cause |
| :- | :- |
| Clicks land far from the target | Coordinates not scaled to the screenshot, or the screenshot was resized between send and execute |
| Model loops, forgets earlier facts | Durable facts not carried in `content`, or old `<observation>` wrappers dropped instead of stripped to text |
| Context window fills up | Image budget not enforced |
| Reasoning leaks into the action | `<think>...</think>` written into `content` instead of read from `message.reasoning` |
| Quality collapses after one bad step | Raw model output replayed in history instead of the parsed result |
| Run stops early, or never ends | Loop exits on an empty tool call instead of on `answer`, or no `answer` tool registered |

Format-specific pitfalls are on each loop guide.


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