Skip to main content
Holo is trained to act as a multi-step agent inside a specific harness, and a few of those conventions have to come along for the model to behave well in yours: an output format, a chat layout for screenshots and tool results, an image budget, and a coordinate convention. Skip any one and quality suffers. Holo supports two output formats, and which ones are available depends on the model:
  • Structured outputs: the model returns a single constrained JSON object per step. Works on Holo3.1 and Holo3.
  • Native function calling: the model returns OpenAI-style tool_calls. Holo3.1 only; Holo3 does not support it.
Pick one and stay in it. The reasoning channel, coordinate convention, and image budget below are identical either way; only how you declare tools and read the model’s output changes. See Output format and tool calls. Set up the OpenAI client first by following the Quickstart.

Reasoning

Holo returns two streams on every call: a thinking trace in message.reasoning and the action in content. Reasoning is essential in agent mode (Holo was trained to plan before each step), so leave it on; reasoning_effort: "medium" is a sensible default.
Past reasoning is dropped between turns by the Qwen 3.5 chat template Holo inherits, so anything the model needs to remember has to flow through content (that is what the note field, below, is for). When re-adding the assistant message to the conversation, push only the parsed output; do not splice the reasoning back in.

Coordinates in [0, 1000]

Send a screenshot at any size. Holo returns coordinates as integers in [0, 1000], normalized to that image. Scale back to pixels using its dimensions:
Origin is top-left. Send and scale against the same image bytes; any resize, crop, or DPI mismatch will misclick. Pick one pixel unit (CSS or device) and stay in it end to end.

Image budget

Keep at most the last 3 screenshots in context; more degrades accuracy. Older screenshots should be replaced with a short text placeholder, while keeping the <observation> wrapper. This works the same in both output formats, since observations are always user messages:

Output format and tool calls

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.

Output JSON

Each step, the model emits one object with three fields:
note is the model’s durable memory: anything from the current screen that future steps will need (URLs, IDs, intermediate answers). Set it to null when nothing new is worth recording. thought is a one-line plan for the next action. tool_call is flat: tool_name is a sibling of the arguments, not nested in an args object.

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 below ships three tools (click, write, answer) for illustration; real agents register a wider toolbox following the same pattern.
Embed the same schema inside the system prompt under an <output_format> block (shown in the loop below). The model was trained with the schema visible in both the prompt and structured_outputs, and dropping either copy noticeably hurts reliability.
Use extra_body={"structured_outputs": {"json": ...}}, not OpenAI native function calling (tools=[...] / tool_choice=...). In this mode the model emits a flat {note, thought, tool_call} object in content, not a tool_calls array.
Pass the schema to structured_outputs, then 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:

Chat layout

User observations alternate with assistant JSON; tool results come back as user messages:Wrap tool results as user messages with <tool_output tool="...">, not as OpenAI tool-role messages.

A complete loop

Plug in your own screenshot() (browser, OS, emulator) and execute(...) dispatcher.

Common pitfalls

Next steps

Element localization

Get click coordinates from a screenshot.

API reference

Endpoint, models, parameters, and limits.

Quickstart

Back to setup and your first call.