# About the Models API
Source: https://hub.hcompany.ai/about-the-models-api
OpenAI-compatible access to the Holo Vision-Language Models for computer use.
The H Company Models API gives developers access to the Holo Vision-Language Models: multimodal models trained to operate real user interfaces across web, desktop, and mobile. Through a single OpenAI-compatible API, you send text, images, or both, and receive structured outputs you can act on directly.
Two models are served today: the fast, open-weight `holo3-1-35b-a3b` (free tier) and the maximum-performance `holo3-122b-a10b`. See [Models](/models) for capabilities, limits, and pricing.
## Two ways to use Holo
| Mode | Pattern | Output | When to use |
| :------------------------------------------------ | :------------------------------------------------------ | :-------------------------------------------------- | :--------------------------------------------------------------------------- |
| [**Agent loop**](/agent-loop) | Multi-turn: conversation + screenshots → next tool call | `{note, thought, tool_call}` or native `tool_calls` | Holo as the brain of an autonomous browser or desktop agent |
| [**Element localization**](/element-localization) | Single-turn: image + target description → coordinates | `{x, y}` in `[0, 1000]` | UI grounding inside any external agent or pipeline (yours or someone else's) |
There is also a third, non-GUI pattern: [Document OCR](/document-ocr), the same endpoint used as a one-shot page transcriber.
## Get started
First request in five minutes.
What is served, limits, and pricing.
Endpoint, conventions, and parameters.
Use Holo in your computer-use harness.
## Model cards and benchmarks
Model cards, weights, and quantized builds.
Mobile, function calling, and local inference.
78.85% on OSWorld-Verified.
Prefer to try the models without writing code? [HoloTab](https://hcompany.ai/meet-holotab) runs Holo directly in your browser without any setup.
# Agent loop
Source: https://hub.hcompany.ai/agent-loop
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](#output-format-and-tool-calls).
Set up the OpenAI client first by following the [Quickstart](/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.
```python Python theme={null}
extra_body={"chat_template_kwargs": {"enable_thinking": True}}
```
```typescript TypeScript theme={null}
// H-specific fields are passed through in the request body
...({ chat_template_kwargs: { enable_thinking: true } } as any)
```
Past reasoning is dropped between turns by the [Qwen 3.5 chat template](https://huggingface.co/Qwen/Qwen3.5-35B-A3B/blob/main/chat_template.jinja) 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:
```python Python theme={null}
abs_x = int((x / 1000) * screenshot.width)
abs_y = int((y / 1000) * screenshot.height)
```
```typescript TypeScript theme={null}
const absX = Math.round((x / 1000) * screenshot.width);
const absY = Math.round((y / 1000) * screenshot.height);
```
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 `` wrapper. This works the same in both output formats, since observations are always `user` messages:
```python Python theme={null}
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 theme={null}
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;
}
}
}
}
```
## 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:
```json theme={null}
{
"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
}
}
```
`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.
```python Python theme={null}
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 theme={null}
// 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"],
};
```
Embed the same schema inside the system prompt under an `` 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.
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:
```python Python theme={null}
schema = Step.model_json_schema()
resp = client.chat.completions.create(
model=MODEL_NAME,
messages=messages, # the system prompt embeds this same schema; see the loop below
extra_body={"structured_outputs": {"json": schema}},
)
step = Step.model_validate_json(resp.choices[0].message.content)
# step.tool_call is now typed as the matching variant (ClickArgs, WriteArgs, ...)
if step.tool_call.tool_name == "answer":
print(step.tool_call.content)
else:
execute(step.tool_call)
```
```typescript TypeScript theme={null}
const resp = await client.chat.completions.create({
model: MODEL_NAME,
messages, // the system prompt embeds this same schema; see the loop below
...({ structured_outputs: { json: schema } } as any),
});
const step = JSON.parse(resp.choices[0].message.content!);
// step.tool_call.tool_name tells you which variant the model picked
if (step.tool_call.tool_name === "answer") {
console.log(step.tool_call.content);
} else {
execute(step.tool_call);
}
```
### Chat layout
User observations alternate with assistant JSON; tool results come back as `user` messages:
| Role | Body |
| :---------- | :------------------------------------------------------------ |
| `system` | your prompt, then the appended `` schema block |
| `user` | `` + screenshot and/or text + ` ` |
| `assistant` | the JSON object: `{note, thought, tool_call}` |
| `user` | `` + result + ` ` |
| `user` | next `` |
| `assistant` | next JSON |
Wrap tool results as `user` messages with ``, not as OpenAI `tool`-role messages.
### A complete loop
Plug in your own `screenshot()` (browser, OS, emulator) and `execute(...)` dispatcher.
````python Python theme={null}
import json, base64
schema = Step.model_json_schema()
system = render_prompt(tools=...) + f"\n\n\n```json\n{json.dumps(schema)}\n```\n "
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": "\n"},
{"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}},
{"type": "text", "text": "\n "},
]})
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'\n{result}\n ',
})
````
```typescript TypeScript theme={null}
const system =
renderPrompt() +
`\n\n\n\`\`\`json\n${JSON.stringify(schema)}\n\`\`\`\n `;
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: "\n" },
{ type: "image_url", image_url: { url: `data:image/png;base64,${b64}` } },
{ type: "text", text: "\n " },
],
});
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: `\n${result}\n `,
});
}
```
Holo3.1 supports the standard OpenAI tool-calling API. Tools are passed via the `tools` parameter, and the model replies with `tool_calls` in the assistant message. This is the most natural format for the model and integrates directly with agent frameworks that already speak OpenAI function calling.
### Declare tools
Pass tool schemas via `tools`, and set `tool_choice="required"` so the model acts on every step. Do not put tool-format examples in the system prompt; the model renders the call in its own native format from `tools` alone, and a conflicting example degrades quality.
```python Python theme={null}
tools = [
{
"type": "function",
"function": {
"name": "click",
"description": "Click at (x, y) coordinates",
"parameters": {
"type": "object",
"properties": {
"element": {"type": "string", "description": "Detailed description of the target UI element"},
"x": {"type": "integer", "description": "X coordinate as integer in [0, 1000]"},
"y": {"type": "integer", "description": "Y coordinate as integer in [0, 1000]"},
},
"required": ["element", "x", "y"],
},
},
},
{
"type": "function",
"function": {
"name": "answer",
"description": "Provide a final answer",
"parameters": {
"type": "object",
"properties": {"content": {"type": "string", "description": "The answer content"}},
"required": ["content"],
},
},
},
]
```
```typescript TypeScript theme={null}
const tools = [
{
type: "function",
function: {
name: "click",
description: "Click at (x, y) coordinates",
parameters: {
type: "object",
properties: {
element: { type: "string", description: "Detailed description of the target UI element" },
x: { type: "integer", description: "X coordinate as integer in [0, 1000]" },
y: { type: "integer", description: "Y coordinate as integer in [0, 1000]" },
},
required: ["element", "x", "y"],
},
},
},
{
type: "function",
function: {
name: "answer",
description: "Provide a final answer",
parameters: {
type: "object",
properties: { content: { type: "string", description: "The answer content" } },
required: ["content"],
},
},
},
] as const;
```
The model returns its action in `message.tool_calls`; each call carries a `function.name`, a JSON `function.arguments` string, and a unique `id`. There is no `note`/`thought` JSON here: planning lives in `message.reasoning`, and any fact the model must carry across turns should be written into the assistant `content`, since the reasoning trace is dropped between turns.
Pass `tools` with `tool_choice="required"`, then read the first call and dispatch on `function.name`:
```python Python theme={null}
resp = client.chat.completions.create(
model=MODEL_NAME,
messages=messages,
tools=tools,
tool_choice="required",
)
call = resp.choices[0].message.tool_calls[0]
args = json.loads(call.function.arguments)
if call.function.name == "answer":
print(args["content"])
else:
execute(call.function.name, args)
```
```typescript TypeScript theme={null}
const resp = await client.chat.completions.create({
model: MODEL_NAME,
messages,
tools,
tool_choice: "required",
});
const call = resp.choices[0].message.tool_calls![0];
const args = JSON.parse(call.function.arguments);
if (call.function.name === "answer") {
console.log(args.content);
} else {
execute(call.function.name, args);
}
```
### Chat layout
Tool results go back as `tool`-role messages keyed by `tool_call_id`, not as `user` messages:
| Role | Body |
| :---------- | :---------------------------------------------------------- |
| `system` | your prompt (no tool-format examples) |
| `user` | `` + screenshot and/or text + ` ` |
| `assistant` | `tool_calls=[{id, function: {name, arguments}}]` |
| `tool` | `tool_call_id` matching the call + result |
| `user` | next `` |
| `assistant` | next `tool_calls` |
### A complete loop
Plug in your own `screenshot()` and `execute(name, args)` dispatcher.
```python Python theme={null}
import json, base64
messages = [{"role": "system", "content": render_prompt()}]
for _ in range(MAX_STEPS):
image_bytes = screenshot()
b64 = base64.b64encode(image_bytes).decode()
messages.append({"role": "user", "content": [
{"type": "text", "text": "\n"},
{"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}},
{"type": "text", "text": "\n "},
]})
trim_to_last_n_images(messages, n=3)
resp = client.chat.completions.create(
model=MODEL_NAME,
messages=messages,
tools=tools,
tool_choice="required",
temperature=0.8,
)
msg = resp.choices[0].message
messages.append(msg)
call = msg.tool_calls[0]
args = json.loads(call.function.arguments)
if call.function.name == "answer":
return args["content"]
result = execute(call.function.name, args)
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": str(result),
})
```
```typescript TypeScript theme={null}
const messages: any[] = [{ role: "system", content: renderPrompt() }];
for (let i = 0; i < MAX_STEPS; i++) {
const b64 = (await screenshot()).toString("base64");
messages.push({
role: "user",
content: [
{ type: "text", text: "\n" },
{ type: "image_url", image_url: { url: `data:image/png;base64,${b64}` } },
{ type: "text", text: "\n " },
],
});
trimToLastNImages(messages, 3);
const resp = await client.chat.completions.create({
model: MODEL_NAME,
messages,
tools,
tool_choice: "required",
temperature: 0.8,
});
const msg = resp.choices[0].message;
messages.push(msg);
const call = msg.tool_calls![0];
const args = JSON.parse(call.function.arguments);
if (call.function.name === "answer") {
return args.content;
}
const result = await execute(call.function.name, args);
messages.push({
role: "tool",
tool_call_id: call.id,
content: String(result),
});
}
```
## Common pitfalls
| Symptom | Likely cause |
| :---------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Clicks land far from the target | Coordinates not scaled to screenshot dimensions, or the screenshot was resized between send and execute |
| Model loops, forgets earlier facts | Durable facts not carried forward (`note` empty in structured mode, or nothing written to `content` in function-calling mode), or older `` wrappers dropped instead of stripped to text |
| Context window fills up | Image budget not enforced |
| Reasoning leaks into the action | `... ` written inline in `content` instead of read from `message.reasoning` |
| Quality collapses after one bad step | Raw model output replayed in history instead of the parsed result |
| (Structured) Model returns free-form text | `extra_body.structured_outputs.json` is missing, or the schema lacks `Literal[tool_name]` discrimination |
| (Structured) Tool result has no effect | Sent as a `tool`-role message instead of a `user` message with a `` wrapper |
| (Function calling) Tool calls come back as plain text | `tool_choice` not set to `required`, or the system prompt contains conflicting tool-format examples |
| (Function calling) Tool result ignored | Sent as a `user` message instead of a `tool`-role message with a matching `tool_call_id` |
## Next steps
Get click coordinates from a screenshot.
Endpoint, models, parameters, and limits.
Back to setup and your first call.
# API reference
Source: https://hub.hcompany.ai/api-reference
Endpoint, authentication, conventions, and the Holo-specific request surface.
The Models API is OpenAI-compatible: point the official OpenAI client (or any compatible library) at H Company's endpoint. You opt into Holo-specific behavior (structured outputs, reasoning, and the coordinate convention) through a few extra request fields and conventions documented here.
## Endpoints
The inference endpoint: parameters, response fields, streaming.
Discover served models, limits, pricing, and deprecation dates at runtime.
## Endpoint and auth
| | |
| :------- | :----------------------------------------------------------------------------------- |
| Base URL | `https://api.hcompany.ai/v1/` |
| Auth | `Authorization: Bearer $HAI_API_KEY` (handled by the OpenAI client) |
| Keys | Create one on [Portal-H](https://portal.hcompany.ai/?product=modelsapi\&source=docs) |
```python Python theme={null}
import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.hcompany.ai/v1/",
api_key=os.environ["HAI_API_KEY"],
)
```
```typescript TypeScript theme={null}
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.hcompany.ai/v1/",
apiKey: process.env.HAI_API_KEY,
});
```
Model IDs, per-model limits, pricing, and tiers live on the [Models](/models) page.
## The two response channels
Holo returns two streams on every call:
The action: the structured JSON object (structured-output mode) or the assistant text.
The thinking trace, when thinking is enabled. Read it for visibility; do not feed it back into the conversation.
The thinking trace is dropped between turns by the chat template Holo inherits. Anything the model must remember has to flow through `content`. See the [Agent loop](/agent-loop#reasoning) for how to carry state forward.
## Conventions
Holo returns click positions as integers normalized to the image you sent. Scale back to pixels with the image's own dimensions. Origin is top-left. Send and scale against the same image bytes: any resize, crop, or DPI mismatch will misplace the point.
Keep at most the last 3 screenshots in context for best accuracy, even though a request accepts up to 5 images. See [the trim helper](/agent-loop#image-budget) in the agent loop.
Structured outputs work on both models; native function calling (`tools` / `tool_calls`) is `holo3-1-35b-a3b` only. Pick one and stay in it: [Agent loop](/agent-loop#output-format-and-tool-calls).
`structured_outputs` and `chat_template_kwargs` are top-level body fields on the wire. The OpenAI SDKs do not know them, so pass them via `extra_body` (Python) or an untyped spread (TypeScript); the SDK merges them into the request body.
## Next steps
Full parameter and response reference.
IDs, limits, pricing, lifecycle.
How to use Holo in your computer-use harness.
# Create an agent
Source: https://hub.hcompany.ai/computer-use-agents/agents/create
POST /api/v2/agents
Create a reusable agent in your own catalog.
Creates a new custom agent in your catalog. Once created, reference it by `name` (e.g. `"agent": "my-research-bot"`) when [creating a session](/computer-use-agents/sessions/create), and its environments, skills, and subagents are pulled from the stored configuration.
**Returns** `201` with the created [Agent](/computer-use-agents/agents/overview) object.
***
## Request body
The body is the [Agent](/computer-use-agents/agents/overview) object. See that page for the full meaning of each field; the constraints that matter when creating one are below.
Catalog identifier, kebab-case with an optional single `org/` namespace prefix. The `h/` prefix is reserved for H's catalog (rejected with `403`) and marks the agent as reserved; any other name creates a custom agent, private to your organization. 1 to 127 characters, immutable after creation.
What the agent does. Read by parent agents to decide what to delegate.
At most one per kind. Each item is a string catalog id or an inline [Browser environment](/computer-use-agents/browser/configuration) spec. Required unless the agent only delegates to `subagents` (a pure [manager](/computer-use-agents/multi-agent) needs none).
Holo model that runs the agent. Defaults to `holo3-122b-a10b`; pass any Holo model id (for example `holo3-1-35b-a3b`) listed in the [Models API](/models). Omit to take the default.
Steering text appended to the system prompt.
Skills available to the agent, as catalog id strings or inline [Skill](/computer-use-agents/skills/overview) specs.
Agents this one can delegate to, as catalog id strings or inline agent specs.
A [JSON Schema](https://json-schema.org/) the agent's final answer must conform to. When set, the agent returns [structured output](/computer-use-agents/structured-output) matching the schema instead of free-form text. Omit it for free-form text; callers can also set or override it per run with session [`overrides`](/computer-use-agents/sessions/create).
[Custom tools](/computer-use-agents/custom-tools) the agent can call from your own code.
***
## Examples
```bash cURL theme={null}
curl -X POST https://agp.eu.hcompany.ai/api/v2/agents \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "my-web-agent",
"description": "Custom web researcher",
"environments": [
{
"id": "browser",
"kind": "web",
"mode": {"type": "visual", "width": 1280, "height": 720},
"start_url": "https://news.ycombinator.com"
}
]
}'
```
```python Python theme={null}
from hai_agents import Client
client = Client()
agent = client.agents.create_agent(
name="my-web-agent",
description="Custom web researcher",
environments=[
{
"id": "browser",
"kind": "web",
"mode": {"type": "visual", "width": 1280, "height": 720},
"start_url": "https://news.ycombinator.com",
}
],
)
print(agent.name)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const agent = await client.agents.createAgent({
name: "my-web-agent",
description: "Custom web researcher",
environments: [
{
id: "browser",
kind: "web",
mode: { type: "visual", width: 1280, height: 720 },
startUrl: "https://news.ycombinator.com",
},
],
});
console.log(agent.name);
```
```json Response theme={null}
{
"name": "my-web-agent",
"description": "Custom web researcher",
"environments": [
{
"id": "browser",
"kind": "web",
"mode": {"type": "visual", "width": 1280, "height": 720},
"start_url": "https://news.ycombinator.com"
}
],
"model": null,
"instructions": null,
"skills": null,
"subagents": null,
"answer_format": null
}
```
***
## Errors
| Status | Cause |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `403` | Attempted to use the reserved `h/` namespace. |
| `409` | An agent with this `name` already exists in your catalog. |
| `422` | Body fails validation; common cases: `environments` empty on an agent that has no `subagents`, a duplicate environment kind, or an invalid `name` shape. |
# Delete an agent
Source: https://hub.hcompany.ai/computer-use-agents/agents/delete
DELETE /api/v2/agents/{agent_name}
Remove an agent from your catalog.
Removes an agent from your catalog. Sessions already running against it are unaffected; new sessions can no longer reference it by name.
**Returns** `204 No Content` on success.
***
## Path parameters
The agent's `name` (e.g. `my-research-bot` or `myorg/web-helper`). Slash-containing names are supported.
***
## Examples
```bash cURL theme={null}
curl -X DELETE https://agp.eu.hcompany.ai/api/v2/agents/my-research-bot \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
client.agents.delete_agent(agent_name="my-research-bot")
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
await client.agents.deleteAgent({ agentName: "my-research-bot" });
```
***
## Errors
| Status | Cause |
| ------ | ------------------------------------------- |
| `403` | The agent is reserved (`h/`) and read-only. |
| `404` | Agent not found or you don't have access. |
# List agents
Source: https://hub.hcompany.ai/computer-use-agents/agents/list
GET /api/v2/agents
Discover available agents from your catalog and the H preset catalog.
Returns a paginated list of agents visible to you: both your custom agents and the built-in H preset catalog.
**Returns** a paginated list of [Agent](/computer-use-agents/agents/overview) objects.
***
## Query parameters
Page number (1-based).
Items per page. Maximum: `1000`.
Sort order. Options: `created_at`, `-created_at`, `agent_name`, `-agent_name`.
***
## Examples
### List all available agents
```bash cURL theme={null}
curl "https://agp.eu.hcompany.ai/api/v2/agents" \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
agents = client.agents.list_agents()
print(agents.items)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const agents = await client.agents.listAgents();
console.log(agents.items);
```
Each item is a full [Agent](/computer-use-agents/agents/overview) object.
```json Response theme={null}
{
"items": [
{
"name": "h/web-surfer-flash",
"description": "General-purpose web browsing agent.",
"environments": ["h/browser"],
"model": null,
"instructions": null,
"skills": ["web-browse", "screenshot"],
"subagents": null,
"answer_format": null
},
{
"name": "price-checker",
"description": "Compares product prices across e-commerce sites.",
"environments": ["h/browser"],
"model": null,
"instructions": null,
"skills": null,
"subagents": null,
"answer_format": null
}
],
"page": 1,
"total": 8
}
```
# Agents
Source: https://hub.hcompany.ai/computer-use-agents/agents/overview
An agent is a reusable configuration that defines what an AI agent can do.
An `agent` holds the configuration a run needs: the environment it acts in, the model that drives it, and optional skills and instructions. Start with a [pre-built agent](#pre-built-agents) from H, or create your own.
Names are scoped to your organization: the agents, skills, and environments you create are visible only within it. H's pre-built agents and environments live under the reserved `h/` namespace (for example `h/web-surfer-flash` and `h/browser`); they are available to every organization and read-only, so modifying one returns `403`. Agents you create have no prefix.
## Configure an agent
| Field | Required | Description |
| --------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name` | Yes | Identifies the agent in your catalog and is the value you pass as `agent` when creating a session. |
| `environments` | Conditional | The surfaces it acts on, like a [browser](/computer-use-agents/browser/configuration). Required unless the agent is a pure [manager](/computer-use-agents/multi-agent) that only delegates to `subagents`. |
| `description` | Yes | A one-line summary of what the agent does. Also used for delegation: a parent agent reads it to decide whether to hand off a task. |
| `model` | No | The Holo model that runs the agent. Defaults to `holo3-122b-a10b`; pass any Holo model id from the [Models API](/models), for example `holo3-1-35b-a3b` for the faster Holo3.1. |
| `instructions` | No | Appended to the system prompt to steer behavior. |
| `skills` | No | Reusable [instruction fragments](/computer-use-agents/skills/overview) the agent loads on demand. |
| `tools` | No | Extra tools the agent can call from your own code. See [Custom tools](/computer-use-agents/custom-tools). |
| `subagents` | No | Specialist agents this one can delegate to. Each runs as its own child session, in parallel, and returns a single answer the manager folds into its own. See [Multi-agent](/computer-use-agents/multi-agent). |
| `answer_format` | No | A [JSON Schema](https://json-schema.org/) the final answer must conform to. When set, the agent returns [structured output](/computer-use-agents/structured-output) matching the schema instead of free-form text. Leave it unset for free-form text; override it per run with session [`overrides`](/computer-use-agents/sessions/create). |
Each `environments`, `skills`, or `subagents` entry is either a string catalog id or an inline object. A reference keeps the definition central and reusable; an inline object is handy for one-offs. For exact field constraints, see [Create an agent](/computer-use-agents/agents/create).
## Create your own
Create an agent once, then reference it by `name` in every session:
```bash cURL theme={null}
curl -X POST https://agp.eu.hcompany.ai/api/v2/agents \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "web-price-finder",
"description": "Finds and reports prices for products, flights, or services on the public web.",
"instructions": "When the user asks for a price, return a single concise line with the amount, currency, and key context (vendor or airline, date, link). Prefer the cheapest matching option. If the price is not visible without login or payment, say so explicitly rather than guessing.",
"model": "holo3-122b-a10b",
"environments": [
{
"id": "price-browser",
"kind": "web",
"mode": {"type": "visual", "width": 1280, "height": 800},
"start_url": "https://www.google.com/travel/flights"
}
],
"skills": ["extract-data"]
}'
```
```python Python theme={null}
from hai_agents import Client
client = Client()
client.agents.create_agent(
name="web-price-finder",
description="Finds and reports prices for products, flights, or services on the public web.",
instructions=(
"When the user asks for a price, return a single concise line with the amount, "
"currency, and key context (vendor or airline, date, link). Prefer the cheapest "
"matching option. If the price is not visible without login or payment, say so "
"explicitly rather than guessing."
),
model="holo3-122b-a10b",
environments=[
{
"id": "price-browser",
"kind": "web",
"mode": {"type": "visual", "width": 1280, "height": 800},
"start_url": "https://www.google.com/travel/flights",
}
],
skills=["extract-data"],
)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
await client.agents.createAgent({
name: "web-price-finder",
description: "Finds and reports prices for products, flights, or services on the public web.",
instructions:
"When the user asks for a price, return a single concise line with the amount, " +
"currency, and key context (vendor or airline, date, link). Prefer the cheapest " +
"matching option. If the price is not visible without login or payment, say so " +
"explicitly rather than guessing.",
model: "holo3-122b-a10b",
environments: [
{
id: "price-browser",
kind: "web",
mode: { type: "visual", width: 1280, height: 800 },
startUrl: "https://www.google.com/travel/flights",
},
],
skills: ["extract-data"],
});
```
## Pre-built agents
H maintains a catalog of configured agents under the `h/` namespace. You can run them as-is, with no setup.
Catalog agents that delegate to subagents (such as `h/deep-search-pro`) build on [Multi-agent](/computer-use-agents/multi-agent).
List them anytime with [`GET /api/v2/agents`](/computer-use-agents/agents/list):
```bash cURL theme={null}
curl "https://agp.eu.hcompany.ai/api/v2/agents" \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
agents = client.agents.list_agents()
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const agents = await client.agents.listAgents();
```
Reference one by name when you create a [session](/computer-use-agents/sessions/overview), and the platform supplies its full configuration:
```bash CLI theme={null}
hai run "Top 3 stories on Hacker News right now?" \
--agent h/web-surfer-flash
```
```bash cURL theme={null}
curl -X POST https://agp.eu.hcompany.ai/api/v2/sessions \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"agent": "h/web-surfer-flash",
"messages": [{"type": "user_message", "message": "Top 3 stories on Hacker News right now?"}]
}'
```
```python Python theme={null}
client.sessions.create_session(
agent="h/web-surfer-flash",
messages=[{"type": "user_message", "message": "Top 3 stories on Hacker News right now?"}],
)
```
```typescript TypeScript theme={null}
await client.sessions.createSession({
body: {
agent: "h/web-surfer-flash",
messages: [{ type: "user_message", message: "Top 3 stories on Hacker News right now?" }],
},
});
```
## Endpoints
| Method | Path | Description |
| -------- | ----------------------- | --------------------------------------------------------- |
| `POST` | `/api/v2/agents` | [Create an agent](/computer-use-agents/agents/create) |
| `GET` | `/api/v2/agents` | [List agents](/computer-use-agents/agents/list) |
| `GET` | `/api/v2/agents/{name}` | [Retrieve an agent](/computer-use-agents/agents/retrieve) |
| `PUT` | `/api/v2/agents/{name}` | [Update an agent](/computer-use-agents/agents/update) |
| `PATCH` | `/api/v2/agents/{name}` | [Patch an agent](/computer-use-agents/agents/patch) |
| `DELETE` | `/api/v2/agents/{name}` | [Delete an agent](/computer-use-agents/agents/delete) |
The list is paginated (`page`, `size`) and returns an `items` / `page` / `total` envelope; sort it with `sort=created_at` or `sort=agent_name`, prefixed with `-` for descending.
# Patch an agent
Source: https://hub.hcompany.ai/computer-use-agents/agents/patch
PATCH /api/v2/agents/{agent_name}
Change individual fields of an agent without resending the full object.
Partial update: only the fields you send change, everything else is preserved. Send a field as `null` to clear it. The merged result is validated like a [full update](/computer-use-agents/agents/update), and `name` is not patchable (renames are not supported).
**Returns** the updated [Agent](/computer-use-agents/agents/overview) object.
***
## Path parameters
The agent's `name` (e.g. `my-research-bot` or `myorg/web-helper`). Slash-containing names are supported.
***
## Request body
Any subset of the [Agent](/computer-use-agents/agents/overview) object's fields except `name`: `description`, `environments`, `model`, `instructions`, `subagents`, `skills`, `answer_format`, `tools`.
***
## Examples
Change the instructions and nothing else:
```bash cURL theme={null}
curl -X PATCH https://agp.eu.hcompany.ai/api/v2/agents/my-research-bot \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"instructions": "Always cite the page you took each claim from."}'
```
```python Python theme={null}
from hai_agents import Client
client = Client()
agent = client.agents.patch_agent(
agent_name="my-research-bot",
instructions="Always cite the page you took each claim from.",
)
print(agent.instructions)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const agent = await client.agents.patchAgent({
agentName: "my-research-bot",
instructions: "Always cite the page you took each claim from.",
});
console.log(agent.instructions);
```
***
## Errors
| Status | Cause |
| ------ | -------------------------------------------------------------------------------------------------- |
| `403` | The agent is reserved (`h/`) and read-only. |
| `404` | Agent not found (or a referenced skill, environment, or subagent isn't), or you don't have access. |
| `422` | The merged spec fails validation, for example `environments` set to an empty list. |
# Retrieve an agent
Source: https://hub.hcompany.ai/computer-use-agents/agents/retrieve
GET /api/v2/agents/{agent_name}
Get the full specification of a single agent.
Retrieves the complete [Agent](/computer-use-agents/agents/overview) object, including its full specification: environments, skills, instructions, and subagent configuration.
**Returns** the [Agent](/computer-use-agents/agents/overview) object if the name is valid and you have access. Returns `404` otherwise.
***
## Path parameters
The agent's `name` (e.g., `h/web` or `my-custom-bot`). Slash-containing names are supported.
***
## Query parameters
When `true`, string references in `environments`, `skills`, and `subagents` are expanded into their full specs in the response. When `false` (the default), they are returned as stored, keeping catalog ids as plain strings.
***
## Examples
### Retrieve an H catalog agent
```bash cURL theme={null}
curl "https://agp.eu.hcompany.ai/api/v2/agents/h/web-surfer-flash" \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
agent = client.agents.get_agent(agent_name="h/web-surfer-flash")
print(agent.name)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const agent = await client.agents.getAgent({ agentName: "h/web-surfer-flash" });
console.log(agent.name);
```
```json Response theme={null}
{
"name": "h/web-surfer-flash",
"description": "General-purpose web browsing agent. Navigates websites, extracts information, fills forms, and completes multi-step web tasks.",
"environments": [
{ "id": "h/browser", "kind": "web", "mode": {"type": "visual", "width": 1280, "height": 720}, "start_url": "https://www.bing.com" }
],
"model": null,
"instructions": null,
"skills": ["web-browse", "screenshot", "form-fill", "extract-data"],
"subagents": null,
"answer_format": null
}
```
### Retrieve a custom agent
```bash cURL theme={null}
curl "https://agp.eu.hcompany.ai/api/v2/agents/price-checker" \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
agent = client.agents.get_agent("price-checker")
```
```typescript TypeScript theme={null}
const agent = await client.agents.getAgent({ agentName: "price-checker" });
```
***
## Use case: inspect before using
Before using an agent in a session, you can inspect its capabilities:
```bash cURL theme={null}
curl "https://agp.eu.hcompany.ai/api/v2/agents/$AGENT_NAME" \
-H "Authorization: Bearer $HAI_API_KEY" \
| jq '{name, description, environments, skills, subagents}'
```
```python Python theme={null}
agent = client.agents.get_agent(agent_name)
print(agent.description, agent.skills)
```
```typescript TypeScript theme={null}
const agent = await client.agents.getAgent({ agentName });
console.log(agent.description, agent.skills);
```
***
## Errors
| Status | Cause |
| ------ | --------------------------------------------------------------------------------------- |
| `404` | Agent not found, or you don't have access (e.g., querying another team's custom agent). |
# Update an agent
Source: https://hub.hcompany.ai/computer-use-agents/agents/update
PUT /api/v2/agents/{agent_name}
Replace the configuration of an agent in your catalog.
Updates an existing agent. This is a **full replacement** of the [Agent](/computer-use-agents/agents/overview) object. The `name` must match the URL identifier: renames are not supported.
**Returns** the updated [Agent](/computer-use-agents/agents/overview) object.
***
## Path parameters
The agent's `name` (e.g. `my-research-bot` or `myorg/web-helper`). Slash-containing names are supported.
***
## Request body
A **full replacement** of the [Agent](/computer-use-agents/agents/overview) object. The `name` in the body must equal the URL identifier. Any field you omit is reset to its default, not preserved: leaving out `instructions`, `model`, `skills`, or `answer_format` clears them. To change individual fields without resending the rest, use [Patch](/computer-use-agents/agents/patch) instead.
***
## Examples
```bash cURL theme={null}
curl -X PUT https://agp.eu.hcompany.ai/api/v2/agents/my-research-bot \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "my-research-bot",
"description": "Researches a topic and returns a sourced summary.",
"environments": ["h/browser"],
"instructions": "Always cite the page you took each claim from.",
"skills": ["web-browse", "extract-data"]
}'
```
```python Python theme={null}
from hai_agents import Client
client = Client()
agent = client.agents.update_agent(
agent_name="my-research-bot",
name="my-research-bot",
description="Researches a topic and returns a sourced summary.",
environments=["h/browser"],
instructions="Always cite the page you took each claim from.",
skills=["web-browse", "extract-data"],
)
print(agent.name)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const agent = await client.agents.updateAgent({
agentName: "my-research-bot",
body: {
name: "my-research-bot",
description: "Researches a topic and returns a sourced summary.",
environments: ["h/browser"],
instructions: "Always cite the page you took each claim from.",
skills: ["web-browse", "extract-data"],
},
});
console.log(agent.name);
```
```json Response theme={null}
{
"name": "my-research-bot",
"description": "Researches a topic and returns a sourced summary.",
"environments": ["h/browser"],
"model": null,
"instructions": "Always cite the page you took each claim from.",
"skills": ["web-browse", "extract-data"],
"subagents": null,
"answer_format": null
}
```
***
## Errors
| Status | Cause |
| ------ | ------------------------------------------------------------------------------------- |
| `400` | The `name` in the body does not match the URL identifier (renames are not supported). |
| `403` | The agent is reserved (`h/`) and read-only. |
| `404` | Agent not found or you don't have access. |
| `422` | Body fails validation; common cases: `environments` empty, invalid `name` shape. |
# Complete a profile upload
Source: https://hub.hcompany.ai/computer-use-agents/browser-profiles/complete-upload
POST /api/v2/browser-profiles/{profile_id}/complete-upload
Finalize a browser profile after its archive is uploaded.
Finalizes a profile after you have uploaded its archive to the presigned target from
[Initiate upload](/computer-use-agents/browser-profiles/initiate-upload). The platform verifies the
uploaded archive and records the profile metadata.
**Returns** `201` with the created profile object (see
[Retrieve](/computer-use-agents/browser-profiles/retrieve) for the full field list).
***
## Path parameters
The `profile_id` returned by [Initiate upload](/computer-use-agents/browser-profiles/initiate-upload).
***
## Request body
Human-readable label for the profile.
Browser the profile was captured with (for example `chromium`, `firefox`, `webkit`, or
`selenium`). Validated against the set of supported browsers.
Browser version string the profile was captured with (for example `131`).
Optional free-text description.
Optional key-value metadata for your own bookkeeping.
***
## Examples
```bash cURL theme={null}
curl -X POST "https://agp.eu.hcompany.ai/api/v2/browser-profiles/a1b2c3d4-5678-90ab-cdef-1234567890ab/complete-upload" \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "acme-prod-login",
"browser_name": "chromium",
"browser_version": "131",
"labels": {"team": "qa"}
}'
```
```python Python theme={null}
from hai_agents import Client
client = Client()
profile = client.browser_profiles.complete_browser_profile_upload(
profile_id="a1b2c3d4-5678-90ab-cdef-1234567890ab",
name="acme-prod-login",
browser_name="chromium",
browser_version="131",
labels={"team": "qa"},
)
print(profile.id)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const profile = await client.browserProfiles.completeBrowserProfileUpload({
profileId: "a1b2c3d4-5678-90ab-cdef-1234567890ab",
name: "acme-prod-login",
browserName: "chromium",
browserVersion: "131",
labels: { team: "qa" },
});
console.log(profile.id);
```
```json Response theme={null}
{
"id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"name": "acme-prod-login",
"description": null,
"browser_name": "chromium",
"browser_version": "131",
"s3_path": "browser-profiles/finished/org_123/a1b2c3d4-5678-90ab-cdef-1234567890ab/profile.zip",
"file_size_bytes": 102400,
"checksum": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"usage_count": 0,
"last_used_at": null,
"labels": {"team": "qa"},
"is_default": false,
"created_at": "2026-06-16T14:30:00Z",
"updated_at": "2026-06-16T14:30:00Z"
}
```
***
## Errors
| Status | Cause |
| ------ | ------------------------------------------------------------------------------------------------------------ |
| `404` | No pending upload found for this `profile_id`, or you don't have access. |
| `422` | Body failed validation (for example an unsupported `browser_name`), or the archive was not found in storage. |
# Delete a browser profile
Source: https://hub.hcompany.ai/computer-use-agents/browser-profiles/delete
DELETE /api/v2/browser-profiles/{profile_id}
Remove a browser profile.
Deletes a browser profile and its stored archive. Agents in your organization can no longer
load it into future sessions.
**Returns** `204 No Content` on success.
***
## Path parameters
The profile's `id` (UUID).
***
## Examples
```bash cURL theme={null}
curl -X DELETE https://agp.eu.hcompany.ai/api/v2/browser-profiles/a1b2c3d4-5678-90ab-cdef-1234567890ab \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
client.browser_profiles.delete_browser_profile(profile_id="a1b2c3d4-5678-90ab-cdef-1234567890ab")
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
await client.browserProfiles.deleteBrowserProfile({
profileId: "a1b2c3d4-5678-90ab-cdef-1234567890ab",
});
```
***
## Errors
| Status | Cause |
| ------ | ------------------------------------------- |
| `404` | Profile not found or you don't have access. |
# Get the default browser profile
Source: https://hub.hcompany.ai/computer-use-agents/browser-profiles/get-default
GET /api/v2/browser-profiles/default
Fetch your default profile for a browser.
Fetches your [default profile](/computer-use-agents/browser/profiles#default-profiles) for a browser, or `404` when none exists yet. A default doesn't have to be set explicitly: the first session created with `use_default_browser_profile: true` auto-creates one.
**Returns** the [profile object](/computer-use-agents/browser-profiles/retrieve).
***
## Query parameters
Browser flavor to look up the default for, for example `chromium`.
***
## Examples
```bash cURL theme={null}
curl "https://agp.eu.hcompany.ai/api/v2/browser-profiles/default?browser_name=chromium" \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
profile = client.browser_profiles.get_default_browser_profile(browser_name="chromium")
print(profile.id, profile.name)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const profile = await client.browserProfiles.getDefaultBrowserProfile({
browserName: "chromium",
});
console.log(profile.id, profile.name);
```
***
## Errors
| Status | Cause |
| ------ | ------------------------------------------- |
| `404` | No default profile is set for this browser. |
# Initiate a profile upload
Source: https://hub.hcompany.ai/computer-use-agents/browser-profiles/initiate-upload
POST /api/v2/browser-profiles/initiate-upload
Get a presigned URL to upload a browser profile archive.
Starts the [profile upload flow](/computer-use-agents/browser/profiles#uploading-a-profile).
Returns a presigned upload target so you can send the profile archive **directly to object
storage**, and the bytes never pass through the Agent API. No request body is required.
After uploading the archive, call
[Complete upload](/computer-use-agents/browser-profiles/complete-upload) with the returned
`profile_id` to finalize the profile.
**Returns** `200` with the presigned upload target.
`upload_url` and `upload_fields` are passed through from object storage unchanged, so send them exactly as returned, with the file as the last form field. They expire after `upload_expires_in` seconds.
***
## Response
Id (UUID) reserved for the profile. Pass it to [Complete upload](/computer-use-agents/browser-profiles/complete-upload).
Presigned object-storage URL to `POST` the archive to.
Form fields that must accompany the upload, sent before the file as `multipart/form-data`.
Seconds until the presigned target expires.
***
## Examples
```bash cURL theme={null}
curl -X POST https://agp.eu.hcompany.ai/api/v2/browser-profiles/initiate-upload \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
upload = client.browser_profiles.initiate_browser_profile_upload()
print(upload.profile_id, upload.upload_url)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const upload = await client.browserProfiles.initiateBrowserProfileUpload();
console.log(upload.profileId, upload.uploadUrl);
```
```json Response theme={null}
{
"profile_id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"upload_url": "https://storage.eu.hcompany.ai/browser-profiles",
"upload_fields": {
"key": "browser-profiles/pending/org_123/a1b2c3d4-5678-90ab-cdef-1234567890ab/profile.zip",
"Content-Type": "application/zip",
"x-amz-checksum-algorithm": "SHA256",
"tagging": "status pending ",
"bucket": "browser-profiles",
"policy": "eyJ...",
"x-amz-algorithm": "AWS4-HMAC-SHA256",
"x-amz-credential": "AKIA.../20260206/eu-west-1/s3/aws4_request",
"x-amz-date": "20260206T000000Z",
"x-amz-signature": "abcd1234"
},
"upload_expires_in": 3600
}
```
# List browser profiles
Source: https://hub.hcompany.ai/computer-use-agents/browser-profiles/list
GET /api/v2/browser-profiles
Browse your organization's browser profiles.
Returns the browser profiles owned by your organization, with offset-based pagination.
**Returns** an object with `total`, `limit`, `offset`, and a `profiles` array of
[profile objects](/computer-use-agents/browser-profiles/retrieve).
***
## Query parameters
Maximum number of profiles to return. Between `1` and `1000`.
Number of profiles to skip before collecting the page.
***
## Examples
```bash cURL theme={null}
curl "https://agp.eu.hcompany.ai/api/v2/browser-profiles?limit=10&offset=0" \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
page = client.browser_profiles.list_browser_profiles(limit=10, offset=0)
for profile in page.profiles:
print(profile.id, profile.name)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const page = await client.browserProfiles.listBrowserProfiles({ limit: 10, offset: 0 });
for (const profile of page.profiles) {
console.log(profile.id, profile.name);
}
```
```json Response theme={null}
{
"total": 1,
"limit": 10,
"offset": 0,
"profiles": [
{
"id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"name": "acme-prod-login",
"description": null,
"browser_name": "chromium",
"browser_version": "131",
"s3_path": "browser-profiles/finished/org_123/a1b2c3d4-5678-90ab-cdef-1234567890ab/profile.zip",
"file_size_bytes": 102400,
"checksum": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"usage_count": 3,
"last_used_at": "2026-06-16T13:05:00Z",
"labels": {"team": "qa"},
"is_default": false,
"created_at": "2026-06-16T14:30:00Z",
"updated_at": "2026-06-16T14:30:00Z"
}
]
}
```
# Retrieve a browser profile
Source: https://hub.hcompany.ai/computer-use-agents/browser-profiles/retrieve
GET /api/v2/browser-profiles/{profile_id}
Fetch a browser profile by id.
Fetches a single browser profile.
**Returns** the profile object.
***
## Path parameters
The profile's `id` (UUID).
***
## The browser profile object
Unique profile identifier (UUID).
Human-readable label.
Optional free-text description.
Browser the profile was captured with (for example `chromium`).
Browser version string the profile was captured with.
Internal storage path of the profile archive.
Size of the stored archive in bytes.
SHA-256 checksum of the stored archive, computed at upload time.
Number of times the profile has been loaded into a session.
ISO 8601 timestamp of the last time the profile was loaded, or `null` if never used.
Key-value metadata you set on the profile.
Whether this profile is your [default](/computer-use-agents/browser/profiles#default-profiles) for its `browser_name`. Set and cleared via the [set-default](/computer-use-agents/browser-profiles/set-default) and [unset-default](/computer-use-agents/browser-profiles/unset-default) endpoints.
ISO 8601 creation timestamp.
ISO 8601 timestamp of the last change.
***
## Examples
```bash cURL theme={null}
curl "https://agp.eu.hcompany.ai/api/v2/browser-profiles/a1b2c3d4-5678-90ab-cdef-1234567890ab" \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
profile = client.browser_profiles.get_browser_profile(profile_id="a1b2c3d4-5678-90ab-cdef-1234567890ab")
print(profile.name, profile.browser_name, profile.usage_count)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const profile = await client.browserProfiles.getBrowserProfile({
profileId: "a1b2c3d4-5678-90ab-cdef-1234567890ab",
});
console.log(profile.name, profile.browserName, profile.usageCount);
```
```json Response theme={null}
{
"id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"name": "acme-prod-login",
"description": null,
"browser_name": "chromium",
"browser_version": "131",
"s3_path": "browser-profiles/finished/org_123/a1b2c3d4-5678-90ab-cdef-1234567890ab/profile.zip",
"file_size_bytes": 102400,
"checksum": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"usage_count": 3,
"last_used_at": "2026-06-16T13:05:00Z",
"labels": {"team": "qa"},
"is_default": false,
"created_at": "2026-06-16T14:30:00Z",
"updated_at": "2026-06-16T14:30:00Z"
}
```
***
## Errors
| Status | Cause |
| ------ | ------------------------------------------- |
| `404` | Profile not found or you don't have access. |
# Set the default browser profile
Source: https://hub.hcompany.ai/computer-use-agents/browser-profiles/set-default
PUT /api/v2/browser-profiles/{profile_id}/default
Mark a profile as your default for its browser.
Marks a profile as your [default](/computer-use-agents/browser/profiles#default-profiles) for its `browser_name`. Sessions created with `use_default_browser_profile: true` on the [Browser environment](/computer-use-agents/browser/configuration) load this profile.
You have at most one default per browser, so setting a new default replaces the previous one for the same `browser_name` — no need to unset it first.
**Returns** the updated [profile object](/computer-use-agents/browser-profiles/retrieve) with `is_default: true`.
***
## Path parameters
The profile's `id` (UUID).
***
## Examples
```bash cURL theme={null}
curl -X PUT "https://agp.eu.hcompany.ai/api/v2/browser-profiles/a1b2c3d4-5678-90ab-cdef-1234567890ab/default" \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
profile = client.browser_profiles.set_default_browser_profile(
profile_id="a1b2c3d4-5678-90ab-cdef-1234567890ab",
)
print(profile.is_default)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const profile = await client.browserProfiles.setDefaultBrowserProfile({
profileId: "a1b2c3d4-5678-90ab-cdef-1234567890ab",
});
console.log(profile.isDefault);
```
```json Response theme={null}
{
"id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"name": "acme-prod-login",
"description": null,
"browser_name": "chromium",
"browser_version": "131",
"s3_path": "browser-profiles/finished/org_123/a1b2c3d4-5678-90ab-cdef-1234567890ab/profile.zip",
"file_size_bytes": 102400,
"checksum": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"usage_count": 3,
"last_used_at": "2026-06-16T13:05:00Z",
"labels": {"team": "qa"},
"is_default": true,
"created_at": "2026-06-16T14:30:00Z",
"updated_at": "2026-07-08T09:00:00Z"
}
```
***
## Errors
| Status | Cause |
| ------ | ------------------------------------------- |
| `404` | Profile not found or you don't have access. |
# Unset the default browser profile
Source: https://hub.hcompany.ai/computer-use-agents/browser-profiles/unset-default
DELETE /api/v2/browser-profiles/{profile_id}/default
Clear the default flag on a profile.
Clears the [default](/computer-use-agents/browser/profiles#default-profiles) flag on a profile. The profile itself is untouched; only the default marking is removed. The next session created with `use_default_browser_profile: true` for that browser auto-creates a fresh empty default rather than reusing this profile.
**Returns** the updated [profile object](/computer-use-agents/browser-profiles/retrieve) with `is_default: false`.
***
## Path parameters
The profile's `id` (UUID).
***
## Examples
```bash cURL theme={null}
curl -X DELETE "https://agp.eu.hcompany.ai/api/v2/browser-profiles/a1b2c3d4-5678-90ab-cdef-1234567890ab/default" \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
profile = client.browser_profiles.unset_default_browser_profile(
profile_id="a1b2c3d4-5678-90ab-cdef-1234567890ab",
)
print(profile.is_default)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const profile = await client.browserProfiles.unsetDefaultBrowserProfile({
profileId: "a1b2c3d4-5678-90ab-cdef-1234567890ab",
});
console.log(profile.isDefault);
```
***
## Errors
| Status | Cause |
| ------ | ------------------------------------------- |
| `404` | Profile not found or you don't have access. |
# Configure the browser
Source: https://hub.hcompany.ai/computer-use-agents/browser/configuration
Define a Browser environment: its fields, modes, and actions.
The Browser (`kind: "web"`) is the environment H ships today: a managed web browser the platform provisions per session, or, with [local control](/computer-use-agents/browser/local-control), Chrome on your own machine. Reference a [built-in browser](/computer-use-agents/environments/overview#built-in-environments) by catalog name (like `"h/browser"`), or define one inline in an agent's `environments` list.
When you define a Browser inline, only `id` is required; every other field has a default. Reference a catalog entry instead and it supplies them for you.
| Field | Default | Description |
| ----------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `id` | required | Catalog identifier for the environment. |
| `kind` | `"web"` | Environment type; a browser is `web`. |
| `start_url` | `"https://www.bing.com"` | Initial URL to open. |
| `headless` | `false` | Run the browser without a visible window. |
| `mode` | `{"type": "visual"}` | How the agent perceives and drives the browser. An object keyed by `type` (`visual` or `text`); see [Modes](#modes). |
| `vault_id` | `null` | Id of a [vault](/computer-use-agents/vaults/overview) to bind to this browser, letting the agent sign in to sites with secrets resolved from the vault. Must reference a vault in your organization. Cloud-hosted browsers only. Omit to run without secret access. |
| `browser_profile_id` | `null` | Id of a [browser profile](/computer-use-agents/browser/profiles) to load into this browser, restoring saved cookies and storage so the agent starts the session already signed in. Must reference a profile in your organization. Cloud-hosted browsers only. Omit to start with a fresh profile. |
| `use_default_browser_profile` | `false` | Load your [default browser profile](/computer-use-agents/browser/profiles#default-profiles) for this browser instead of naming one, auto-creating an empty one on first use. The session saves its final state back automatically when it ends (best-effort: concurrent sessions run read-only). Mutually exclusive with `browser_profile_id`. Cloud-hosted browsers only. |
| `persist_browser_profile` | `false` | Save the session's final browser state (cookies, storage) back into the loaded profile when it ends. Requires `browser_profile_id` or `use_default_browser_profile`. Best-effort: only one active session at a time may persist a given profile — if another writer is active, the session starts read-only instead of failing. See [Persisting state back](/computer-use-agents/browser/profiles#persisting-state-back-into-the-profile). |
| `network` | `null` | Network settings for the session. Holds `managed_proxy` to have H provision a proxy for the browser's egress, or `proxy_url` to route through your own (set only one). Applied when a cloud browser session is provisioned. See [Proxy](/computer-use-agents/browser/proxy). |
| `host` | `"cloud"` | Where the browser runs: `cloud` on H infrastructure, or `user_device` for Chrome on your own machine. See [Local browser](/computer-use-agents/browser/local-control). |
| `session_id` | `null` | Id of the command channel a `user_device` browser is served on. The Python SDK sets it when it starts the connection for you; set it yourself only to attach to one you run with [`hai local browser`](/computer-use-agents/browser/local-control#run-a-session). |
## Modes
At each step the agent receives a fresh observation of the page, then chooses one action. The `mode` field sets what that observation contains and which [actions](#actions) are on the table.
| Mode | What the agent sees | How it acts | Reach for it when |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------- |
| `visual` (default) | A screenshot of the viewport (`1200×1200` by default), with the current URL and open tabs. Set `markdown: true` to also include the page's text in the same observation. | Points at on-screen targets; the platform resolves each one to exact click and type coordinates. | General web work: clicking, filling forms, anything that needs the rendered page. |
| `text` | The page as text only, no screenshot, split into chunks of about `chunk_size` characters (`20000` by default). | Reads and pages through the chunks and follows links by URL. Read-only: no clicking or typing. | Reading and research at scale: search, scraping, link-heavy navigation. |
* **Cost and speed track the screenshots.** `text` mode sends no images, so it uses the fewest tokens and runs fastest; `visual` sends a screenshot every step, and `markdown: true` adds the full page text on top of it. Default to `visual`, and switch to `text` when the task is pure reading.
* **`text` mode pages through long content.** Rather than scrolling, the page is cut into `chunk_size`-sized chunks; the agent moves between them and each observation tells it which chunk it is on. Raise `chunk_size` to fit more per step, at the cost of more tokens per observation.
* **Watch what the agent saw.** Every observation rides the [event stream](/computer-use-agents/sessions/events#observation-shapes), as a `web` observation in `visual` mode and a `textual_web` observation in `text` mode, so you can replay each step exactly as the agent perceived it.
### Mode fields
`mode` is an object selected by `type`. Each shape carries only the fields that apply to it, so illegal combinations cannot be expressed.
`visual` (default) renders screenshots and acts by viewport coordinates:
| Field | Default | Description |
| ---------- | ---------- | ----------------------------------------------------------------------- |
| `type` | `"visual"` | Selects visual mode. |
| `width` | `1200` | Viewport width in pixels. Must be a positive integer. |
| `height` | `1200` | Viewport height in pixels. Must be a positive integer. |
| `markdown` | `false` | Also include the viewport's text as markdown alongside each screenshot. |
`text` serves read-only paginated markdown with no screenshots:
| Field | Default | Description |
| ------------ | -------- | ------------------------------------------------------------------- |
| `type` | `"text"` | Selects text mode. |
| `chunk_size` | `20000` | Characters of page text shown per page. Must be a positive integer. |
```json Text-mode browser theme={null}
{ "id": "research-browser", "kind": "web", "mode": { "type": "text", "chunk_size": 20000 } }
```
## Actions
Each mode fixes the set of actions available to the agent. It chooses them autonomously as it works; you never call them directly and there is no per-agent tool list to configure. To shape how it uses them, set the agent's [`instructions`](/computer-use-agents/agents/overview).
| Action | Description | Visual | Text |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :----: | :--: |
| `go_to_web` | Navigate to a URL. | ✓ | ✓ |
| `go_back_web` | Go back in the browser history. | ✓ | ✓ |
| `refresh_web` | Refresh the current page. | ✓ | ✓ |
| `switch_tab_web` | Switch to another tab, or open a new one. | ✓ | ✓ |
| `close_tab_web` | Close a tab. | ✓ | ✓ |
| `click_web` | Click at viewport coordinates. | ✓ | |
| `write` | Focus an input at coordinates and type into it. | ✓ | |
| `fill_secret_at` | Fill a vault-resolved secret (e.g. `password`, `totp`) into a field at coordinates so the agent can sign in on your behalf. The value is injected directly into the page and never enters the agent's context. Offered only when a [vault](/computer-use-agents/vaults/overview) is bound to the browser via `vault_id` and can match a credential for the current page. | ✓ | |
| `select_option` | Pick an option from a native `` dropdown. | ✓ | |
| `move_mouse_web` | Move the mouse to reveal hovers, tooltips, or menus. | ✓ | |
| `press_keys_web` | Press keys or keyboard shortcuts. | ✓ | |
| `scroll_web` | Scroll the page or a nested scrollable container. | ✓ | |
| `ctrl_f_web` | Jump to the next on-page match of a text query. | ✓ | |
| `reader_mode` | Extract the page's main content as clean markdown. | ✓ | |
| `find_in_page` | Find a text query and jump to the chunk that contains it. | | ✓ |
| `switch_chunk` | Page forward or backward through the page's text chunks. | | ✓ |
| `wait_web` | Pause for the page to settle (up to 60 seconds). | ✓ | ✓ |
## Next steps
Start a session already signed in from saved cookies and storage.
Egress through an H-managed proxy or one you operate.
# Run the agent in your own browser
Source: https://hub.hcompany.ai/computer-use-agents/browser/local-control
Let an agent drive Chrome on your own machine.
With local browser control, the agent drives Chrome on **your own machine** instead of a browser H hosts in the cloud, keeping the same [`agent`](/computer-use-agents/agents/overview) and [`session`](/computer-use-agents/sessions/overview) lifecycle you already use. Run a session that uses a local browser from the [`hai-agents`](/computer-use-agents/sdks) Python SDK (the only SDK with local control today) and it launches Chrome and connects it to H for you.
Use it when the work has to happen where you are: a site you are signed in to on your machine, or anything behind your local network that a cloud browser cannot reach.
The agent acts in a real Chrome on your machine: it can browse, run page scripts, and read that browser's cookies and storage. Stop a run at any time by [cancelling the session](/computer-use-agents/sessions/overview#lifecycle): `hai sessions cancel `.
## How it works
A local browser is a normal [browser environment](/computer-use-agents/browser/configuration) with its `host` set to `user_device`. When a session starts with one, the SDK opens a connection inside your Python process. The connection receives the agent's actions and carries them out in the Chrome it controls, and it is what ties this particular session to this particular machine. Everything else is unchanged: [observe and steer](/computer-use-agents/observe-and-steer) the run and read its answer just as you would a remote one.
A Python process drives one local browser at a time. Starting a new session that uses the local browser while an earlier one is still running hands the browser to the new session and cancels the earlier one.
Because it drives your machine, a local browser skips the cloud-provisioning fields: [vaults](/computer-use-agents/vaults/overview) and [browser profiles](/computer-use-agents/browser/profiles) apply to cloud-hosted browsers only. Your local Chrome's own logins and cookies fill that role.
The browser driver is an optional extra, provided by the [`hai-drivers`](https://pypi.org/project/hai-drivers/) package:
```bash Install theme={null}
pip install "hai-agents[browser]"
```
A local browser agent is a normal [agent](/computer-use-agents/agents/overview) whose browser environment sets `host` to `user_device`; everything else about the spec is unchanged. [Create it](/computer-use-agents/agents/create) in your catalog:
```python Python theme={null}
from hai_agents import Client
client = Client()
agent = client.agents.create_agent(
name="local-web",
description="Drives a browser on my own machine.",
environments=[
{
"id": "my-laptop",
"kind": "web",
"host": "user_device",
}
],
)
```
In Python, run a session with the agent object from the previous step. There is nothing else to set up; the SDK starts Chrome if needed and drives it there. From the CLI, serve the browser with `hai local browser` and point the agent at the `session_id` it prints.
```python Python theme={null}
result = client.run_session(
agent=agent,
messages="Open news.ycombinator.com and summarize the top story",
)
print(result.status, result.answer)
```
```bash CLI theme={null}
pip install "hai-agents[cli,browser]"
# Serve the browser and leave it running; it prints the session_id it serves.
hai local browser
# From another terminal, route the agent's browser environment here.
hai run "Open news.ycombinator.com and summarize the top story" \
--agent local-web \
-o 'agent.environments[kind=web].session_id='
```
The session behaves like any other: [observe and steer](/computer-use-agents/observe-and-steer) it, read [changes](/computer-use-agents/sessions/changes), or watch it in [Agent View](/computer-use-agents/observe-and-steer#watch-a-run). The Python connection closes when your process exits; Chrome stays open.
Auto-connect covers agents defined inline in `run_session`, `start_session`, or `create_session`. A [registered](/computer-use-agents/agents/overview) agent referenced by name, or a session started from the web app or another machine, expects its `user_device` environment to carry the `session_id` of a machine served with `hai local browser`, as in the CLI tab. Set `HAI_AUTO_BRIDGE=0` to opt out of auto-connect entirely.
## The Chrome it drives
The SDK attaches to a Chrome instance with remote debugging open on port `9222`. If none is running, it launches one with its own profile in `~/.hai/chrome-profile`. That profile persists across runs: sign in to a site once and the agent finds you signed in next time. Your everyday Chrome profile is never touched; Chrome does not allow remote debugging on it.
To drive a different Chrome, start it yourself before the session and the SDK attaches to it instead:
```bash macOS theme={null}
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
--remote-debugging-port=9222 --user-data-dir="$HOME/chrome-for-agents"
```
```bash Linux theme={null}
google-chrome --remote-debugging-port=9222 --user-data-dir="$HOME/chrome-for-agents"
```
Chrome only opens the debugging port on a profile passed with `--user-data-dir`, so pick a dedicated directory and keep it for the logins you want the agent to have.
## Next steps
Modes, start URL, profiles, and the rest of the browser environment.
Drive the whole desktop on your machine, not just the browser.
Watch a local run, redirect it mid-task, and read the answer.
The session lifecycle, including how to cancel a run.
# Bring your own browser profile
Source: https://hub.hcompany.ai/computer-use-agents/browser/profiles
Start a browser session from saved cookies and storage.
A **browser profile** captures a browser's saved state (cookies, local storage, and session data) as a reusable, organization-scoped bundle. Upload one once, then set [`browser_profile_id`](/computer-use-agents/browser/configuration) so the agent starts a session already authenticated instead of signing in from scratch every run. Sessions can also [write their final state back](#persisting-state-back-into-the-profile) into the profile, so it stays fresh from run to run instead of aging out.
Don't want to manage profiles at all? Your [default profile](#default-profiles) gives you a browser that remembers you across sessions with zero setup — no upload, no ids to track.
Where a [vault](/computer-use-agents/vaults/overview) injects individual secrets at the moment the agent fills a login form, a profile restores the **entire** logged-in state up front. Reach for a vault when the agent should sign in with credentials; reach for a profile to carry an existing, already-authenticated session forward.
Profiles belong to your organization: any agent in the org can load one, and they persist across sessions until you delete them.
## Creating a profile
A profile is built from a browser's **user-data directory**: the folder Chrome uses to store cookies, local storage, saved logins, and extensions.
Don't upload your personal Chrome directory (`~/Library/Application Support/Google/Chrome` on macOS, `~/.config/google-chrome` on Linux). It holds every password, session token, and history entry from your day-to-day browsing.
The simplest way to get a clean directory is to launch a **separate Chrome instance** pointed at a throwaway `--user-data-dir`. This runs alongside your normal browser without touching your real profile.
Start Chrome with a new empty directory. It opens a brand-new profile with no cookies, logins, or extensions.
`--password-store=basic` is what makes the profile portable: it tells Chrome to encrypt cookies and saved passwords with a built-in key instead of your OS keychain. The session runner uses the same store, so cookies saved this way decrypt correctly when the agent loads the profile. Without it, cookies are encrypted with a machine-specific key that the runner can't reproduce, and the agent starts logged out.
```bash macOS theme={null}
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
--user-data-dir="$HOME/agent-profile"
```
```bash Linux theme={null}
google-chrome \
--user-data-dir="$HOME/agent-profile" \
--password-store=basic
```
`--password-store=basic` only takes effect on **Linux**. On **macOS and Windows** Chrome always encrypts cookies with the OS keychain (Keychain / DPAPI), so a profile built with desktop Chrome there loads **without its auth cookies** — no flag can change that. To carry cookies forward on macOS or Windows, build the profile in Docker instead (see [macOS and Windows: build the profile in Docker](#macos-and-windows-build-the-profile-in-docker-to-keep-cookies)).
In that window, do exactly what you want the agent to inherit: log in to the target sites, dismiss cookie banners, install extensions, adjust settings. Everything is written into `~/agent-profile`.
Cleanly quit the browser so it flushes its state to disk, then archive the directory:
```bash theme={null}
cd "$HOME/agent-profile"
zip -r ../profile.zip .
```
The resulting `profile.zip` is what you upload below.
### macOS and Windows: build the profile in Docker to keep cookies
`--password-store=basic` has no effect on macOS or Windows, so a profile built with desktop Chrome there loses its cookies. Build it inside a Linux container instead — the container has no OS keychain, so Chromium encrypts cookies with the same portable key the session runner uses. This needs [Docker](https://www.docker.com/).
```bash theme={null}
# Start a throwaway Linux Chromium you can drive from your browser.
# The tag is pinned to Chromium 150.0.7871.124 — the version the session runner
# uses. Don't use :latest: it tracks a newer Chromium, and a profile built on a
# newer version won't load on the runner.
docker run -d --name agent-profile -p 127.0.0.1:3000:3000 \
-e CHROME_CLI="--password-store=basic" \
-v "$HOME/agent-profile:/config" \
lscr.io/linuxserver/chromium:11e602ae-ls45
# Open http://localhost:3000 and log in to the sites the agent needs.
# Signal Chromium to flush cookies to disk, wait for it, then stop and zip:
docker exec agent-profile pkill -INT -o chromium
sleep 3
docker rm -f agent-profile
cd "$HOME/agent-profile/.config/chromium" && zip -r ~/profile.zip .
```
Send Chromium `SIGINT` (`pkill -INT`) and give it a moment **before** stopping the container. Chromium only writes cookies to disk on a clean shutdown or an occasional autosave — stopping the container straight away (`docker stop` sends `SIGTERM`, `docker rm -f` sends `SIGKILL`) can discard the logins you just created.
Upload the resulting `~/profile.zip` as described below.
## Uploading a profile
The archive is a `.zip` of the user-data directory you built above. It can hold cookies and saved logins, so treat it as a secret.
A profile archive can be large, so the bytes are uploaded **directly to object storage** through a presigned URL and never pass through the Agent API. The flow is three steps:
Call [`POST /initiate-upload`](/computer-use-agents/browser-profiles/initiate-upload). It returns a `profile_id`, a presigned `upload_url`, and the `upload_fields` you must include with the upload.
```bash cURL theme={null}
INIT=$(curl -s -X POST https://agp.eu.hcompany.ai/api/v2/browser-profiles/initiate-upload \
-H "Authorization: Bearer $HAI_API_KEY")
PROFILE_ID=$(echo "$INIT" | jq -r .profile_id)
UPLOAD_URL=$(echo "$INIT" | jq -r .upload_url)
```
```python Python theme={null}
from hai_agents import Client
client = Client()
upload = client.browser_profiles.initiate_browser_profile_upload()
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const upload = await client.browserProfiles.initiateBrowserProfileUpload();
```
`POST` your profile `.zip` to `upload_url` as `multipart/form-data`, including every entry from `upload_fields`. This request goes to object storage, not to the Agent API. The archive must be `application/zip` and at most **1 GB**; storage rejects anything else.
```bash cURL theme={null}
args=()
while IFS= read -r line; do
args+=(--form-string "$line")
done <<< "$(echo "$INIT" | jq -r '.upload_fields | to_entries[] | "\(.key)=\(.value)"')"
curl -sS --fail-with-body -w "upload: HTTP %{http_code}\n" \
-X POST "$UPLOAD_URL" "${args[@]}" -F "file=@profile.zip"
# Healthy upload will return `upload: HTTP 204`
```
```python Python theme={null}
import requests
with open("profile.zip", "rb") as f:
resp = requests.post(
upload.upload_url,
data=upload.upload_fields,
files={"file": ("profile.zip", f, "application/zip")},
)
resp.raise_for_status()
```
```typescript TypeScript theme={null}
import { readFileSync } from "fs";
const form = new FormData();
for (const [key, value] of Object.entries(upload.uploadFields)) {
form.append(key, value as string);
}
form.append("file", new Blob([readFileSync("profile.zip")]), "profile.zip");
const resp = await fetch(upload.uploadUrl, { method: "POST", body: form });
if (!resp.ok) throw new Error(`upload failed: ${resp.status}`);
```
The presigned `upload_url` and `upload_fields` are passed through to object storage exactly as returned, so do not modify them. They expire after `upload_expires_in` seconds, so upload promptly after initiating.
Call [`POST /{profile_id}/complete-upload`](/computer-use-agents/browser-profiles/complete-upload) with the profile metadata (`name`, `browser_name`, `browser_version`) **within 1 day** of uploading. The platform verifies the uploaded archive and creates the profile record. Until you complete it, the upload is held as pending and is deleted automatically after 1 day.
```bash cURL theme={null}
curl -X POST "https://agp.eu.hcompany.ai/api/v2/browser-profiles/$PROFILE_ID/complete-upload" \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "acme-prod-login", "browser_name": "chromium", "browser_version": "131"}'
```
```python Python theme={null}
profile = client.browser_profiles.complete_browser_profile_upload(
profile_id=upload.profile_id,
name="acme-prod-login",
browser_name="chromium",
browser_version="131",
)
print(profile.id)
```
```typescript TypeScript theme={null}
const profile = await client.browserProfiles.completeBrowserProfileUpload({
profileId: upload.profileId,
name: "acme-prod-login",
browserName: "chromium",
browserVersion: "131",
});
console.log(profile.id);
```
Once created, manage your profiles with the [browser profile endpoints](/computer-use-agents/browser-profiles/list): list, retrieve, and delete them.
## Persisting state back into the profile
By default a profile is **read-only**: every session starts from the profile's saved state, and whatever the agent does during the run (new cookies, refreshed tokens, dismissed banners) is discarded when the session ends. Over time the profile's sessions expire and agents are back to login walls.
Set `persist_browser_profile: true` alongside `browser_profile_id` (or `use_default_browser_profile`) on the [Browser environment](/computer-use-agents/browser/configuration) to flip that: when the session ends, the browser's final state is written back into the profile, replacing its previous contents. The next session that loads the profile picks up exactly where the last one left off.
```json Browser that keeps its profile fresh theme={null}
{
"id": "acme-browser",
"kind": "web",
"browser_profile_id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"persist_browser_profile": true
}
```
A few rules keep concurrent runs from corrupting each other's state:
* **One writer at a time.** Only one active session may persist a given profile; the session holds an exclusive write lock on the profile until it ends. A second session requesting `persist_browser_profile` on the same profile still starts, but runs **read-only** for that round — check the session's effective persist state if the write-back matters to you. Any number of sessions may load the profile read-only in parallel.
* **Last write wins.** Persisting replaces the profile's entire stored state with the session's final state; it is not merged.
* **Decided at create time.** Persistence can only be requested when the session is created, not when attaching to an existing session.
* **Best-effort on stop.** The write-back happens as the session stops. If it fails, the session still stops cleanly and the profile keeps its previous state.
Sessions that follow your [default profile](#default-profiles) persist automatically — no flag needed; see below.
## Default profiles
Every user has a **default profile** per browser flavor (for example one for `chromium`): a browser that remembers you from session to session, with zero setup. Opt a Browser environment into it with `use_default_browser_profile: true`:
```json Browser that follows the default profile theme={null}
{
"id": "acme-browser",
"kind": "web",
"use_default_browser_profile": true
}
```
* **First use bootstraps it.** If you have no default yet, an empty profile is created and marked as your default automatically when the session starts — no upload, no extra API call. Log in to your sites once (for example through the session's live view) and the state carries forward from then on.
* **Sessions save back automatically.** A session that follows the default profile writes its final browser state (cookies, storage) back when it ends. The write-back is best-effort: when several default-profile sessions run at once, the first to start becomes the writer and the others load the profile read-only for that round — they never fail over it.
* **Explicit persist is best-effort too.** `persist_browser_profile: true` requests the writer role but never blocks the run: if another session already holds the [write lock](#persisting-state-back-into-the-profile), the session starts read-only instead of failing. For a run that must persist (for example a scheduled login-refresh job), verify the session's effective persist state after creation and reschedule if it was downgraded.
`use_default_browser_profile` is mutually exclusive with `browser_profile_id` — name a profile or follow the default, not both. Auto-created defaults appear in your [profile list](/computer-use-agents/browser-profiles/list) with a `default--...` name; deleting one simply means your next default-profile session starts over with a fresh empty default.
### Choosing your own default
If you'd rather your agents follow a profile you curated — for example one [uploaded from a real machine](#creating-a-profile) — promote it with [`PUT /browser-profiles/{profile_id}/default`](/computer-use-agents/browser-profiles/set-default); setting a new default for the same browser replaces the previous one. Check what is currently set with [`GET /browser-profiles/default`](/computer-use-agents/browser-profiles/get-default), and clear it with [`DELETE /browser-profiles/{profile_id}/default`](/computer-use-agents/browser-profiles/unset-default). Profile objects report their status in the `is_default` field. You can only mark profiles you created yourself as your default.
# Route the browser through a proxy
Source: https://hub.hcompany.ai/computer-use-agents/browser/proxy
Egress the browser through an H-managed proxy or one you operate.
Route the browser's egress through a proxy when the target site blocks datacenter IPs or geofences by region, or when a run needs traffic from a specific network. The [browser environment](/computer-use-agents/browser/configuration)'s `network` field supports two options:
* **Managed proxy** (`network.managed_proxy`): H provisions a proxy for the session. You pick the pool, country, and stickiness; the platform handles the provider and credentials.
* **Bring your own** (`network.proxy_url`): route through a proxy you operate, credentials inline.
The two are mutually exclusive; set only one. Either applies when a cloud browser session is provisioned; a [local browser](/computer-use-agents/browser/local-control) uses your machine's own network.
## Managed proxy
Ask H to provision the proxy by setting `network.managed_proxy`. The request carries intent only; credentials are resolved server-side and never appear in your requests, session data, or logs.
| Field | Type | Default | Description |
| ------------------------------- | -------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `network.managed_proxy.pool` | string | `"residential"` | IP pool to egress from: `residential` (real-ISP addresses, best for sites that block datacenter ranges) or `datacenter`. Requesting a pool that is not available in your environment fails with `400`. |
| `network.managed_proxy.country` | string \| null | `null` | Two-letter ISO 3166-1 code (like `"US"`) to egress from a specific country. Omit for any location. Invalid codes fail with `422`. |
| `network.managed_proxy.sticky` | boolean | `true` | Keep the same exit IP for the whole session. Applies to residential pools; datacenter endpoints manage rotation themselves. |
Create a catalog browser environment that egresses from a US residential IP:
```bash cURL theme={null}
curl -X POST https://agp.eu.hcompany.ai/api/v2/environments \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"id": "us-browser",
"kind": "web",
"network": {"managed_proxy": {"pool": "residential", "country": "US"}}
}'
```
```python Python theme={null}
from hai_agents import BrowserNetwork, Client, ManagedProxySelection
client = Client()
environment = client.environments.create_environment(
id="us-browser",
kind="web",
network=BrowserNetwork(
managed_proxy=ManagedProxySelection(pool="residential", country="US"),
),
)
print(environment.id)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const environment = await client.environments.createEnvironment({
kind: "web",
id: "us-browser",
network: { managedProxy: { pool: "residential", country: "US" } },
});
console.log(environment.id);
```
## Bring your own proxy
Route through a proxy you operate by setting `network.proxy_url`.
| Field | Type | Default | Description |
| ------------------- | -------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `network.proxy_url` | string \| null | `null` | Bring-your-own proxy URL for browser egress, with any credentials inline: `http://user:pass@host:port`. HTTP, HTTPS, and SOCKS schemes are accepted. Treat the value as a secret. |
Create a catalog browser environment that routes through your proxy:
```bash cURL theme={null}
curl -X POST https://agp.eu.hcompany.ai/api/v2/environments \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"id": "proxy-browser",
"kind": "web",
"network": {"proxy_url": "http://user:pass@proxy.example.com:8080"}
}'
```
```python Python theme={null}
from hai_agents import BrowserNetwork, Client
client = Client()
environment = client.environments.create_environment(
id="proxy-browser",
kind="web",
network=BrowserNetwork(proxy_url="http://user:pass@proxy.example.com:8080"),
)
print(environment.id)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const environment = await client.environments.createEnvironment({
kind: "web",
id: "proxy-browser",
network: { proxyUrl: "http://user:pass@proxy.example.com:8080" },
});
console.log(environment.id);
```
## Next steps
The rest of the browser environment: fields, modes, and actions.
# Changelog
Source: https://hub.hcompany.ai/computer-use-agents/changelog
Notable changes to the Computer-Use Agents API and SDKs.
**Session retention controls.** Set `delete_after_min` to delete a finished session after a delay, and `delete_screenshot_after_min` to expire its screenshots sooner. Both default to 30 days; `null` keeps data indefinitely. See [Create a session](/computer-use-agents/sessions/create).
**Faster local desktop.** Screenshots are downscaled and recompressed on your machine before upload, cutting per-step latency on high-resolution displays. Tune with `--max-width`, `--image-format`, and `--quality` on `hai local desktop`. See [Local desktop](/computer-use-agents/desktop/local-control).
**Sturdier local control (Python SDK and CLI).** `hai doctor` diagnoses login, platform access, and local-control prerequisites with fix-it hints. `hai local stop` (or double-Esc during a desktop turn) cancels the in-flight local session, sessions auto-cancel when your process exits, and auto-started sessions get default `max_steps` / `max_time_s` budgets. On macOS, missing Accessibility or Screen Recording permissions fail fast at startup instead of silently doing nothing.
**Local control.** Let an agent drive the browser or desktop on your own machine, not just one H runs in the cloud. Set an environment's `host` to `user_device` and run a session from the Python SDK; it launches Chrome and connects your machine to H automatically. See [Local browser](/computer-use-agents/browser/local-control) and [Local desktop](/computer-use-agents/desktop/local-control).
**Persistent browser profiles.** Browser environments can now write the session's final browser state (cookies, storage) back into the loaded profile: set `persist_browser_profile: true` alongside `browser_profile_id`, and each run refreshes the profile for the next one instead of starting from an aging snapshot. An exclusive write lock means only one active session at a time may persist a given profile (concurrent read-only use is unaffected); when the lock is held, a new persist session starts read-only instead of failing. See [Persisting state back](/computer-use-agents/browser/profiles#persisting-state-back-into-the-profile).
**Default browser profiles.** Every user now has a zero-setup browser that remembers them: set `use_default_browser_profile: true` on a Browser environment and the session loads your default profile — auto-creating an empty one on first use — then saves its final state back automatically when it ends (best-effort: concurrent sessions run read-only instead of failing). Log in once, and later sessions start already signed in. Prefer a curated profile? Promote it with [`PUT /browser-profiles/{profile_id}/default`](/computer-use-agents/browser-profiles/set-default); profile objects now report `is_default`, and [get-default](/computer-use-agents/browser-profiles/get-default) and [unset-default](/computer-use-agents/browser-profiles/unset-default) endpoints round out the surface. See [Default profiles](/computer-use-agents/browser/profiles#default-profiles).
**Scheduled sessions.** Create cron schedules that start a session on each fire: five-field cron expressions evaluated in an IANA timezone, with pause/resume, manual trigger, and a per-schedule run history showing whether each fire created a session or was skipped. See [Schedules](/computer-use-agents/schedules/overview).
**Managed proxies.** Browser environments can now egress through an H-provisioned proxy: set `network.managed_proxy` with a `pool` (`residential` or `datacenter`), an optional `country`, and `sticky` to keep one exit IP for the session. The platform resolves the provider and credentials server-side, so you never handle them. Mutually exclusive with the existing `network.proxy_url`. See [Proxy](/computer-use-agents/browser/proxy).
**Webhook delivery health.** The webhook object now reports its delivery state: `last_delivery_status`, `last_delivery_error`, `last_delivery_at`, `last_success_at`, and `consecutive_failures`. Poll [retrieve](/computer-use-agents/webhooks/retrieve) to monitor an endpoint without waiting for it to be disabled. See [Webhooks](/computer-use-agents/webhooks/overview).
**Queue sessions instead of 429.** Over-quota session creates are now accepted with the new `queued` status instead of rejected. They start automatically, oldest first, as slots free up, fire webhooks on every transition, and can be cancelled while queued. Set `queue: false` on [create](/computer-use-agents/sessions/create) to keep the old 429 behavior. See [Queued sessions](/computer-use-agents/observe-and-steer#queued-sessions).
**Granular webhook events, retries, ping, secret rotation.** Subscribe to specific event types like `session.completed` or `session.awaiting_tool_results` instead of the `session.status_updated` firehose ([event catalog](/computer-use-agents/webhooks/events)). Failed deliveries now retry with backoff, endpoints that keep failing are disabled automatically, and you can [ping](/computer-use-agents/webhooks/ping) an endpoint or [rotate its secret](/computer-use-agents/webhooks/rotate) without downtime. See [Webhooks](/computer-use-agents/webhooks/overview).
**Machine-readable failures.** Failed sessions now carry an `error_code` (`environment_error`, `no_answer`, `answer_validation`, `timeout`, `internal`) next to the human-readable `error`, and every answer reports the agent's self-assessed `outcome` (`success`, `partial`, `infeasible`, `blocked`). Build retry logic against codes, not strings. See [Read how the run ended](/computer-use-agents/observe-and-steer#read-how-the-run-ended).
**Browser `headless` option.** Browser environments accept `headless: true` to run without a visible window (default `false`). See [Configuration](/computer-use-agents/browser/configuration).
Registry packages skipped this version; its changes shipped in the 1.0.4 packages.
**Browse the `h/` catalog without an API key.** Listing agents, skills, and environments no longer requires authentication for the reserved `h/` catalog; authenticated calls additionally return your organization's own entries.
**CLI agent picker.** `hai run` without `--agent` now lists the live agent catalog and prompts for a choice instead of assuming a default agent.
**Nested Browser `mode`.** A Browser's `mode` is now an object keyed by `type`, so viewport and text settings live with the mode they belong to instead of as flat siblings:
* `visual` (default): `{"type": "visual", "width": 1200, "height": 1200, "markdown": false}`. Set `markdown: true` to include the page's text alongside each screenshot (this replaces the old `multimodal` mode).
* `text`: `{"type": "text", "chunk_size": 20000}`, where `chunk_size` replaces the old top-level `page_chars`.
The old flat shape (`"mode": "visual"` with sibling `width`, `height`, `page_chars`) still parses, so existing integrations keep working. See [Modes](/computer-use-agents/browser/configuration#modes).
# Build with coding assistants
Source: https://hub.hcompany.ai/computer-use-agents/coding-skills
Install the hai-agents skill so Claude Code, Cursor, and other assistants know the H APIs and help you build use cases.
The `hai-agents` skill teaches your coding assistant H's APIs, so instead of guessing at endpoints it scaffolds working sessions, agents, and environments from a plain-language prompt.
The same `SKILL.md` works in Claude Code, Cursor, and Hermes; only the install step differs. It ships from the [`hcompai/computer-use-agents-demos`](https://github.com/hcompai/computer-use-agents-demos) repo, which also publishes it as a Claude Code plugin (marketplace `hai-skills`).
The skill's source on GitHub: `SKILL.md` plus the reference docs Claude loads. Start here.
## What your assistant learns
Once active, `hai-agents` gives your assistant grounded knowledge of:
| Area | Coverage |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| **Portal** | Auth, organizations, invitations, billing, and API-key management, including an automated login script that writes your key into `.env`. |
| **Agent platform** | Sessions, agents, skills, environments, vaults, and `/changes` long-polling, the building blocks in this guide. |
| **SDKs** | The `hai-agents` Python and TypeScript clients: `run_session` / `runSession`, session handles, events, and error classes. |
| **Agent View** | The run-replay workflow for reviewing and sharing what an agent did. |
The skill triggers automatically when your conversation matches its description: asking about H sessions or `hk-...` keys pulls it in without you naming it.
## Install
Register the marketplace, then install the skill in any Claude Code session:
```bash theme={null}
/plugin marketplace add hcompai/computer-use-agents-demos
/plugin install hai-agents@hai-skills
```
`/plugin` is a built-in Claude Code command that opens an interactive menu. Type it directly in your prompt. The marketplace must be added first, otherwise `hai-skills` is unknown.
Cursor (2.4+) reads skills from `.cursor/skills/` in your project. Copy the skill folder in, then reload the window:
```bash theme={null}
git clone https://github.com/hcompai/computer-use-agents-demos
mkdir -p .cursor/skills
cp -r computer-use-agents-demos/skills/hai-agents .cursor/skills/hai-agents
```
Then **Cmd/Ctrl+Shift+P → "Developer: Reload Window"**. The skill activates automatically when a request matches its description.
Hermes reads agentskills.io skills from `~/.hermes/skills/`. Copy the skill folder in, then restart Hermes:
```bash theme={null}
git clone https://github.com/hcompai/computer-use-agents-demos
mkdir -p ~/.hermes/skills
cp -r computer-use-agents-demos/skills/hai-agents ~/.hermes/skills/hai-agents
```
Hermes re-scans `~/.hermes/skills/` on startup. The skill activates automatically when a request matches its description.
## Build a use case
With the skill installed, describe the workflow you want in plain language and let your assistant scaffold it against the live APIs. In Claude Code you can invoke the plugin's slash command directly (in Cursor or Hermes, a plain prompt triggers the skill the same way):
> *❯ /hai-agents:hai-agents "Add one iPhone 17 Pro to my Amazon.com cart"*
Because the skill knows the session lifecycle, region defaults, and SDK surface, the code it produces uses the right endpoints and helpers instead of hand-rolled HTTP.
It scaffolds TypeScript or Python. The video at the top of this page shows it building a TypeScript availability check from a single prompt:
> *❯ /hai-agents:hai-agents "Generate TypeScript code that navigates to jacquemus.com, finds the France × Nike football jersey, and checks its availability in sizes S and XXL."*
Generated code: [`examples/product_availability/src/index.ts`](https://github.com/hcompai/computer-use-agents-demos/blob/main/examples/product_availability/src/index.ts)
## Next steps
Run your first session end to end in under 5 minutes.
The typed Python and TypeScript clients and CLI the skill builds on.
Reusable instruction fragments you attach to a running agent.
More recipes: QA via CLI, schema-driven extraction, counterfeit detection.
# Give an agent custom tools
Source: https://hub.hcompany.ai/computer-use-agents/custom-tools
Extend the agent's toolbox with functions from your own code: the agent calls them, the SDK executes them, the run continues with the result.
Every agent works out of a toolbox. Its [environment](/computer-use-agents/environments/overview) supplies the core of it (a browser environment brings navigation, clicking, typing) and the agent layers its own built-in tools on top. Custom tools are the part you add: functions from your own code that complement that toolbox with anything it can't reach on its own, like querying your database, calling an internal API, or looking up a customer record.
Pass a function to the SDK and the agent uses it like any other tool: when it decides to call one mid-run, the SDK executes your function locally and the run continues with the result. The full loop is handled for you.
## With the SDKs
Pass your functions via `tools`; the schema is derived from the signature and docstring in Python, or declared with `tool()` in TypeScript.
```python Python theme={null}
from hai_agents import Client
def get_order_status(order_id: str) -> str:
"""Look up the status of an order in our system."""
return db.orders.get(order_id).status
client = Client()
result = client.run_session(
agent="h/web-surfer-flash",
messages="Check order 4242 and email the customer if it shipped.",
tools=[get_order_status],
)
print(result.answer)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient, tool } from "hai-agents";
const getOrderStatus = tool({
name: "get_order_status",
description: "Look up the status of an order in our system.",
inputSchema: {
type: "object",
properties: { order_id: { type: "string" } },
required: ["order_id"],
},
fn: async ({ order_id }) => db.orders.get(order_id).status,
});
const client = new HaiAgentsClient();
const result = await client.runSession({
agent: "h/web-surfer-flash",
messages: "Check order 4242 and email the customer if it shipped.",
tools: [getOrderStatus],
});
console.log(result.answer);
```
Functions may be sync or async, and exceptions are reported to the agent as tool errors instead of crashing the run.
Tools execute in the process that polls the session, so they only run while your program is waiting on `run_session` / `runSession` (or a handle's `wait_for_completion` / `waitForCompletion` with the same `tools`). Execution is at-least-once: if posting a result fails and the wait is retried, the tool may run again, so prefer idempotent tool functions for side-effecting operations.
## Over the raw API
Without an SDK to run the loop, you declare the tools, watch for the agent to call one, and post the result yourself.
Declare the tools when [creating the session](/computer-use-agents/sessions/create), inline on the agent or via the `agent.tools` override for a registered agent:
```json Session create body theme={null}
{
"agent": {
"name": "support-agent",
"environments": [{ "kind": "web" }],
"tools": [
{
"name": "get_order_status",
"description": "Look up the status of an order in our system.",
"input_schema": {
"type": "object",
"properties": { "order_id": { "type": "string" } },
"required": ["order_id"]
}
}
]
},
"messages": "Check order 4242 and email the customer if it shipped."
}
```
Long-poll [`changes`](/computer-use-agents/sessions/changes) for an `ActiveStateChangeEvent` whose `data.state` is `"awaiting_tool_results"`. Its `data.pending_tool_calls` lists each pending call as a `{ tool_name, args, id }` object:
```json Pending tool call event theme={null}
{
"type": "ActiveStateChangeEvent",
"data": {
"state": "awaiting_tool_results",
"pending_tool_calls": [
{ "tool_name": "get_order_status", "args": { "order_id": "4242" }, "id": "call_1" }
]
},
"timestamp": "2026-06-01T15:14:05Z"
}
```
Execute the call and [post the result](/computer-use-agents/sessions/tool-results), echoing the pending call back as `tool_req`:
```bash theme={null}
curl -X POST "https://agp.eu.hcompany.ai/api/v2/sessions/$SESSION_ID/tool_results" \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"kind": "tool_result", "tool_req": {"tool_name": "get_order_status", "args": {"order_id": "4242"}, "id": "call_1"}, "result": "shipped"}'
```
Send several at once with `{"type": "batch", "results": [...]}`. Report a failure as an `error_event` instead of a `tool_result`; it carries `error`, `origin` (both required), and the echoed `tool_req`:
```json Tool error report theme={null}
{ "kind": "error_event", "error": "Order not found", "origin": "custom_tools", "tool_req": { "tool_name": "get_order_status", "args": { "order_id": "4242" }, "id": "call_1" } }
```
The agent resumes once every pending call has a result; calls still unresolved when the run ends (for example on `max_time_s`) fail with a model-visible error. Posting to a finished session returns `409`.
# Run the agent on your own desktop
Source: https://hub.hcompany.ai/computer-use-agents/desktop/local-control
Let an agent drive the mouse, keyboard, and screen on your own machine.
With local desktop control, the agent works on the machine in front of you. It drives your real mouse, keyboard, and screen, with the same [`agent`](/computer-use-agents/agents/overview) and [`session`](/computer-use-agents/sessions/overview) lifecycle you already use. Run a session that uses a local desktop from the [`hai-agents`](/computer-use-agents/sdks) Python SDK (the only SDK with local control today) and it connects your machine to H for you. Works on macOS, Windows, and Linux.
Use it when the task lives outside the browser: native apps, the file system, or multi-window flows on a machine you control.
The agent controls your whole desktop: it can move the mouse, type, read anything on screen, run shell commands, and read or write files. Prefer a dedicated machine over your primary one. Stop a run at any time by [cancelling the session](/computer-use-agents/sessions/overview#lifecycle): `hai sessions cancel `.
## How it works
A local desktop is an [environment](/computer-use-agents/environments/overview) with its `kind` set to `desktop` and `host` set to `user_device`. When a session starts with one, the SDK opens a connection inside your Python process. The connection receives the agent's actions and carries them out on the real desktop, and it is what ties this particular session to this particular machine. Everything else is unchanged: [observe and steer](/computer-use-agents/observe-and-steer) the run and read its answer just as you would a remote one.
A Python process serves one local desktop session at a time. Starting a new session that uses the local desktop while an earlier one is still running hands the desktop to the new session and cancels the earlier one.
The desktop driver is an optional extra, provided by the [`hai-drivers`](https://pypi.org/project/hai-drivers/) package:
```bash Install theme={null}
pip install "hai-agents[desktop]"
```
A local desktop agent is a normal [agent](/computer-use-agents/agents/overview) with an environment whose `kind` is `desktop` and whose `host` is `user_device`; everything else about the spec is unchanged. [Create it](/computer-use-agents/agents/create) in your catalog:
```python Python theme={null}
from hai_agents import Client
client = Client()
agent = client.agents.create_agent(
name="local-desktop",
description="Drives the desktop on my own machine.",
environments=[
{
"id": "my-laptop",
"kind": "desktop",
"host": "user_device",
}
],
)
```
In Python, run a session with the agent object from the previous step. There is nothing else to set up; the SDK connects the desktop before the session starts. From the CLI, serve the desktop with `hai local desktop` and point the agent at the `session_id` it prints.
```python Python theme={null}
result = client.run_session(
agent=agent,
messages="Open Notes and write a short summary of today's standup",
)
print(result.status, result.answer)
```
```bash CLI theme={null}
pip install "hai-agents[cli,desktop]"
# Serve the desktop and leave it running; it prints the session_id it serves.
hai local desktop
# From another terminal, route the agent's desktop environment here.
hai run "Open Notes and write a short summary of today's standup" \
--agent local-desktop \
-o 'agent.environments[kind=desktop].session_id='
```
The session behaves like any other: [observe and steer](/computer-use-agents/observe-and-steer) it, read [changes](/computer-use-agents/sessions/changes), or watch it in [Agent View](/computer-use-agents/observe-and-steer#watch-a-run). The Python connection closes when your process exits.
Auto-connect covers agents defined inline in `run_session`, `start_session`, or `create_session`. A [registered](/computer-use-agents/agents/overview) agent referenced by name, or a session started from the web app or another machine, expects its `user_device` environment to carry the `session_id` of a machine served with `hai local desktop`, as in the CLI tab. Set `HAI_AUTO_BRIDGE=0` to opt out of auto-connect entirely.
The agent controls the real mouse and keyboard and reads the screen, so your operating system has to trust the program running it (your terminal, or the app that launches Python). On macOS, your first session triggers two permission prompts and stops with an error until both are granted. Grant them in **System Settings → Privacy & Security**, then restart the program and run the session again:
* **Accessibility**, to move the mouse and type.
* **Screen Recording**, to read the screen.
On Windows and Linux there is nothing to grant; run the program in a normal desktop session so it can reach the display.
## Next steps
Drive Chrome on your machine the same way.
Watch a local run, redirect it mid-task, and read the answer.
Install the clients and authenticate.
How environments attach to an agent and what each kind contributes.
# Create an environment
Source: https://hub.hcompany.ai/computer-use-agents/environments/create
POST /api/v2/environments
Create a reusable environment in your own catalog.
Creates a new custom environment in your catalog. Once created, reference it by `id` (e.g. `"environments": ["wide-browser"]`) from any agent. Most users define environments inline on the agent instead; use this endpoint to reuse one environment across several agents.
**Returns** `201` with the created [Environment](/computer-use-agents/environments/overview) object.
***
## Request body
The body is a [Browser](/computer-use-agents/browser/configuration) spec.
Catalog identifier, kebab-case with an optional single `org/` namespace prefix. The `h/` prefix is reserved for H's catalog (rejected with `403`). Immutable after creation.
Environment type. Currently only `web`. Defaults to `web`.
Initial URL to open.
Run the browser without a visible window.
How the agent perceives and drives the browser. An object keyed by `type`:
* `visual` (default): `{"type": "visual", "width": 1200, "height": 1200, "markdown": false}`. Set `markdown: true` to include the page's text alongside the screenshot.
* `text`: `{"type": "text", "chunk_size": 20000}`. Read-only paginated markdown, no screenshots.
See [Modes](/computer-use-agents/browser/configuration#modes) and [Mode fields](/computer-use-agents/browser/configuration#mode-fields).
Id of a [vault](/computer-use-agents/vaults/overview) to bind to this browser, letting the agent sign in to sites with secrets resolved from the vault. Must reference a vault in your organization. Omit to run without secret access.
Id of a [browser profile](/computer-use-agents/browser/profiles) to load into this browser, restoring saved cookies and storage so the agent starts the session already signed in. Must reference a profile in your organization. Omit to start with a fresh profile.
Load your [default browser profile](/computer-use-agents/browser/profiles#default-profiles) instead of naming one, auto-creating an empty one on first use. Sessions save their final state back automatically (best-effort). Mutually exclusive with `browser_profile_id`.
Save the session's final browser state back into the loaded profile when it ends. Requires `browser_profile_id` or `use_default_browser_profile`. Best-effort: if another session is already [persisting the profile](/computer-use-agents/browser/profiles#persisting-state-back-into-the-profile), the session starts read-only instead of failing.
Network settings for the browser session. Set only one of:
* `managed_proxy`: have H provision a proxy for browser egress. An object with `pool` (`residential`, default, or `datacenter`), `country` (two-letter ISO code, optional), and `sticky` (keep one exit IP for the session, default `true`). Credentials are resolved server-side and never appear in your requests.
* `proxy_url`: a bring-your-own HTTP/HTTPS/SOCKS proxy URL for browser egress, with any credentials inline (e.g. `http://user:pass@host:port`).
See [Proxy](/computer-use-agents/browser/proxy).
***
## Examples
```bash cURL theme={null}
curl -X POST https://agp.eu.hcompany.ai/api/v2/environments \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"id": "wide-browser",
"kind": "web",
"start_url": "https://www.google.com",
"mode": {"type": "visual", "width": 1920, "height": 1080}
}'
```
```python Python theme={null}
from hai_agents import Client
client = Client()
environment = client.environments.create_environment(
id="wide-browser",
start_url="https://www.google.com",
mode={"type": "visual", "width": 1920, "height": 1080},
)
print(environment.id)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const environment = await client.environments.createEnvironment({
kind: "web",
id: "wide-browser",
startUrl: "https://www.google.com",
mode: { type: "visual", width: 1920, height: 1080 },
});
console.log(environment.id);
```
```json Response theme={null}
{
"id": "wide-browser",
"kind": "web",
"start_url": "https://www.google.com",
"mode": {"type": "visual", "width": 1920, "height": 1080, "markdown": false},
"vault_id": null,
"browser_profile_id": null,
"use_default_browser_profile": false,
"persist_browser_profile": false,
"network": null
}
```
***
## Errors
| Status | Cause |
| ------ | -------------------------------------------------------------------------------- |
| `403` | Attempted to use the reserved `h/` namespace. |
| `409` | An environment with this `id` already exists in your catalog. |
| `422` | Body fails validation; common cases: invalid `id` shape, missing required field. |
# Delete an environment
Source: https://hub.hcompany.ai/computer-use-agents/environments/delete
DELETE /api/v2/environments/{id}
Remove an environment from your catalog.
Removes an environment from your catalog. Sessions already running are unaffected; new sessions and agents can no longer reference it by `id`.
**Returns** `204 No Content` on success.
***
## Path parameters
The environment's `id` (e.g. `wide-browser` or `myorg/wide-browser`). Slash-containing identifiers are supported.
***
## Examples
```bash cURL theme={null}
curl -X DELETE https://agp.eu.hcompany.ai/api/v2/environments/wide-browser \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
client.environments.delete_environment("wide-browser")
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
await client.environments.deleteEnvironment({ id: "wide-browser" });
```
***
## Errors
| Status | Cause |
| ------ | ------------------------------------------------- |
| `403` | The environment is reserved (`h/`) and read-only. |
| `404` | Environment not found or you don't have access. |
# List environments
Source: https://hub.hcompany.ai/computer-use-agents/environments/list
GET /api/v2/environments
Discover available environments.
Returns a paginated list of environments visible to you: both your custom environments and the built-in H preset catalog.
**Returns** a paginated list of [Environment](/computer-use-agents/environments/overview) objects.
***
## Query parameters
Page number (1-based).
Items per page. Maximum: `1000`.
Sort order. Options: `created_at`, `-created_at`, `id`, `-id`.
***
## Examples
```bash cURL theme={null}
curl "https://agp.eu.hcompany.ai/api/v2/environments" \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
page = client.environments.list_environments()
print(page.items)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const page = await client.environments.listEnvironments();
console.log(page.items);
```
Each item is a full [Environment](/computer-use-agents/environments/overview) object.
```json Response theme={null}
{
"items": [
{"id": "h/browser", "kind": "web", "start_url": "https://www.bing.com", "mode": {"type": "visual", "width": 1200, "height": 1200, "markdown": false}, "vault_id": null},
{"id": "h/textual_browser", "kind": "web", "start_url": "https://www.bing.com", "mode": {"type": "text", "chunk_size": 20000}, "vault_id": null}
],
"page": 1,
"total": 2
}
```
# Environments
Source: https://hub.hcompany.ai/computer-use-agents/environments/overview
The surfaces an agent perceives and acts upon.
An environment is what the agent sees and acts on, one step at a time: look at the screen, reason, act.
An agent gets its environments from its `environments` list, where each entry is either a catalog name or an inline environment object. At least one is required, with at most one per kind.
The [Browser](/computer-use-agents/browser/configuration) is the environment H hosts today, and both the browser and the desktop can run on your own machine with [local control](/computer-use-agents/browser/local-control). Cloud desktops for Mac, Windows, and Linux are in [What's next](/computer-use-agents/introduction#whats-next).
## Toolbox
Attaching an environment equips the agent with a toolbox: the actions it can take on that surface, along with built-in guidance on how to use them. The [Browser](/computer-use-agents/browser/configuration), for example, contributes web actions such as navigate, click, type, scroll, and read.
The set of actions is fixed per environment, so you cannot add or remove individual ones. To shape how the agent uses them, set the agent's [`instructions`](/computer-use-agents/agents/overview) or attach your own [skills](/computer-use-agents/skills/overview), which load on demand on top of the built-in guidance.
## Built-in environments
H ships browsers in the catalog. Browse them below, or list them with [`GET /api/v2/environments`](/computer-use-agents/environments/list):
## Create your own
Most agents define an environment inline in their `environments` list. Create a catalog environment instead when you want to reuse one across several agents, then reference it by `id`:
```bash cURL theme={null}
curl -X POST https://agp.eu.hcompany.ai/api/v2/environments \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"id": "wide-browser",
"kind": "web",
"mode": {"type": "visual", "width": 1920, "height": 1080}
}'
```
```python Python theme={null}
from hai_agents import Client
client = Client()
client.environments.create_environment(
id="wide-browser",
kind="web",
mode={"type": "visual", "width": 1920, "height": 1080},
)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
await client.environments.createEnvironment({
id: "wide-browser",
kind: "web",
mode: { type: "visual", width: 1920, height: 1080 },
});
```
## Endpoints
| Method | Path | Description |
| -------- | --------------------------- | --------------------------------------------------------------------- |
| `POST` | `/api/v2/environments` | [Create an environment](/computer-use-agents/environments/create) |
| `GET` | `/api/v2/environments` | [List environments](/computer-use-agents/environments/list) |
| `GET` | `/api/v2/environments/{id}` | [Retrieve an environment](/computer-use-agents/environments/retrieve) |
| `PUT` | `/api/v2/environments/{id}` | [Update an environment](/computer-use-agents/environments/update) |
| `PATCH` | `/api/v2/environments/{id}` | [Patch an environment](/computer-use-agents/environments/patch) |
| `DELETE` | `/api/v2/environments/{id}` | [Delete an environment](/computer-use-agents/environments/delete) |
The list is paginated (`page`, `size`) and returns an `items` / `page` / `total` envelope; sort it with `sort=created_at`, prefixed with `-` for descending.
# Patch an environment
Source: https://hub.hcompany.ai/computer-use-agents/environments/patch
PATCH /api/v2/environments/{id}
Change individual fields of an environment without resending the full spec.
Partial update: only the fields you send change, everything else is preserved. Send a field as `null` to clear it, for example `vault_id: null` to unbind a [vault](/computer-use-agents/vaults/overview). The merged result is validated like a [full update](/computer-use-agents/environments/update), and `id` and `kind` are not patchable.
This makes binding a vault to an existing environment a one-liner, with no need to resend the full spec.
**Returns** the updated [Environment](/computer-use-agents/environments/overview) object.
***
## Path parameters
The environment's `id` (e.g. `wide-browser` or `myorg/wide-browser`). Slash-containing identifiers are supported.
***
## Request body
Any subset of the [Browser](/computer-use-agents/browser/configuration) spec's fields except `id` and `kind`: `start_url`, `headless`, `mode`, `vault_id`, `browser_profile_id`, `use_default_browser_profile`, `persist_browser_profile`, `network`.
***
## Examples
Bind a vault, leaving the rest of the spec untouched:
```bash cURL theme={null}
curl -X PATCH https://agp.eu.hcompany.ai/api/v2/environments/wide-browser \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"vault_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479"}'
```
```python Python theme={null}
from hai_agents import Client
client = Client()
environment = client.environments.patch_environment(
"wide-browser",
vault_id="f47ac10b-58cc-4372-a567-0e02b2c3d479",
)
print(environment.vault_id)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const environment = await client.environments.patchEnvironment({
id: "wide-browser",
vaultId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
});
console.log(environment.vaultId);
```
***
## Errors
| Status | Cause |
| ------ | ---------------------------------------------------------------- |
| `403` | The environment is reserved (`h/`) and read-only. |
| `404` | Environment not found or you don't have access. |
| `422` | The merged spec fails validation, for example an invalid `mode`. |
# Retrieve an environment
Source: https://hub.hcompany.ai/computer-use-agents/environments/retrieve
GET /api/v2/environments/{id}
Get the full specification of an environment.
Retrieves the complete [Environment](/computer-use-agents/environments/overview) object.
**Returns** the Environment object if the identifier is valid and you have access.
***
## Path parameters
The environment's `id` (e.g., `h/browser` or `my-browser`). Slash-containing identifiers are supported.
***
## Examples
```bash cURL theme={null}
curl "https://agp.eu.hcompany.ai/api/v2/environments/h/browser" \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
environment = client.environments.get_environment("h/browser")
print(environment)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const environment = await client.environments.getEnvironment({ id: "h/browser" });
console.log(environment);
```
```json Response theme={null}
{
"id": "h/browser",
"kind": "web",
"start_url": "https://www.bing.com",
"mode": {"type": "visual", "width": 1200, "height": 1200, "markdown": false},
"vault_id": null
}
```
***
## Errors
| Status | Cause |
| ------ | ----------------------------------------------- |
| `404` | Environment not found or you don't have access. |
# Update an environment
Source: https://hub.hcompany.ai/computer-use-agents/environments/update
PUT /api/v2/environments/{id}
Replace the configuration of an environment in your catalog.
Updates an existing environment. This is a **full replacement** of the [Browser](/computer-use-agents/browser/configuration) spec. The `id` must match the URL identifier: renames are not supported.
**Returns** the updated [Environment](/computer-use-agents/environments/overview) object.
***
## Path parameters
The environment's `id` (e.g. `wide-browser` or `myorg/wide-browser`). Slash-containing identifiers are supported.
***
## Request body
A **full replacement** of the [Browser](/computer-use-agents/browser/configuration) spec. The `id` in the body must equal the URL identifier. Any field you omit is reset to its default, not preserved, including `vault_id`: omitting it unbinds any [vault](/computer-use-agents/vaults/overview) currently attached. To change individual fields without resending the rest, use [Patch](/computer-use-agents/environments/patch) instead.
The example below binds a vault while keeping the rest of the spec.
***
## Examples
```bash cURL theme={null}
curl -X PUT https://agp.eu.hcompany.ai/api/v2/environments/wide-browser \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"id": "wide-browser",
"kind": "web",
"start_url": "https://example.com",
"mode": {"type": "visual", "width": 1920, "height": 1080, "markdown": true},
"vault_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
}'
```
```python Python theme={null}
from hai_agents import Client
client = Client()
environment = client.environments.update_environment(
"wide-browser",
id="wide-browser",
start_url="https://example.com",
mode={"type": "visual", "width": 1920, "height": 1080, "markdown": True},
vault_id="f47ac10b-58cc-4372-a567-0e02b2c3d479",
)
print(environment.mode)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const environment = await client.environments.updateEnvironment({
id: "wide-browser",
body: {
kind: "web",
id: "wide-browser",
startUrl: "https://example.com",
mode: { type: "visual", width: 1920, height: 1080, markdown: true },
vaultId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
},
});
console.log(environment.mode);
```
```json Response theme={null}
{
"id": "wide-browser",
"kind": "web",
"start_url": "https://example.com",
"mode": {"type": "visual", "width": 1920, "height": 1080, "markdown": true},
"vault_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
}
```
***
## Errors
| Status | Cause |
| ------ | ----------------------------------------------------------------------------------- |
| `400` | The `id` in the body does not match the URL identifier (renames are not supported). |
| `403` | The environment is reserved (`h/`) and read-only. |
| `404` | Environment not found or you don't have access. |
| `422` | Body fails validation; common cases: invalid `id` shape, missing required field. |
# Handle API errors
Source: https://hub.hcompany.ai/computer-use-agents/errors
How Computer-Use Agents report errors, and what to do about them.
The API uses standard HTTP status codes and a consistent error envelope. When something goes wrong, the response body always has the same shape, so your error-handling code works for every endpoint.
This page covers HTTP-level errors: the request itself was rejected or hit a server fault. A session that was accepted but ended `failed` or `timed_out` reports why through its `error_code`; see [Read how the run ended](/computer-use-agents/observe-and-steer#read-how-the-run-ended).
## Error object
Every error response carries the same envelope: a `message` that summarizes what went wrong, and a `detail` array with one entry per problem:
```json Error envelope theme={null}
{
"message": "Session not found.",
"detail": [
{ "type": "not_found", "message": "Session not found." }
]
}
```
For validation errors (422), each `detail` entry keeps the failing field's path and reason, and `message` flattens them into a single line:
```json Validation error (422) theme={null}
{
"message": "agent: Field required",
"detail": [
{
"type": "missing",
"loc": ["body", "agent"],
"msg": "Field required",
"input": {}
}
]
}
```
Read `message` when you just need something to log or display; reach into `detail` when you need per-field specifics. Requests rejected before they reach the API (for example a missing or invalid key, `401`) may carry only a `message`.
***
## HTTP status codes
### Success codes
| Code | Meaning | Used by |
| ---------------- | ------------------------------------------- | ------------------------------------------ |
| `200 OK` | Request succeeded. | GET endpoints, updates. |
| `201 Created` | Resource created. | POST /sessions, POST /agents. |
| `202 Accepted` | Action accepted, processing asynchronously. | POST /messages, POST /pause, POST /resume. |
| `204 No Content` | Action succeeded, no response body. | DELETE /sessions, DELETE /agents. |
### Client error codes
| Code | Meaning | Common cause | What to do |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400 Bad Request` | Malformed request or invalid parameters. | Sending an event to a session that can't accept it. | Check the request body against the endpoint docs. |
| `401 Unauthorized` | Missing or invalid API key. | No `Authorization` header, or expired key. | Verify your API key is correct and properly formatted. |
| `402 Payment Required` | Monthly token budget exhausted. | Creating (or restarting) a session after your organization consumed its plan's tokens. | The `detail` entry carries `limit`, `used`, and `window_end`; wait for the window to reset or upgrade your plan. See [Plans and limits](/computer-use-agents/plans-and-limits#token-usage). |
| `403 Forbidden` | Valid credentials, insufficient permissions. | Trying to modify a reserved `h/` agent. | Check that your role allows this operation. |
| `404 Not Found` | Resource doesn't exist or isn't visible to you. | Wrong session ID, or querying another team's resource. | Verify the ID and that you have access. |
| `409 Conflict` | Resource already exists, or an identical idempotent request is still in flight. | Creating an agent with a duplicate name, or retrying a request with the same `Idempotency-Key` before the first one finished. | Use a unique identifier, fetch the existing resource, or wait for the in-flight request to complete. |
| `422 Unprocessable Entity` | Request body fails validation. | Missing required fields, wrong types. | Check the `detail` array for specific field errors. |
| `429 Too Many Requests` | Concurrency limit exceeded with [queueing](/computer-use-agents/observe-and-steer#queued-sessions) declined, or the session queue is full. | Too many sessions running simultaneously. | Leave `queue` at its default to queue instead, wait for running sessions to complete, or request a quota increase. See [Plans and limits](/computer-use-agents/plans-and-limits#concurrent-sessions). |
### Server error codes
| Code | Meaning | What to do |
| ------------------------- | --------------------------------- | ----------------------------------------- |
| `502 Bad Gateway` | Upstream service error. | Retry after a brief pause. |
| `503 Service Unavailable` | Temporary capacity issue. | Check the `Retry-After` header and retry. |
| `504 Gateway Timeout` | Request took too long to process. | Retry the request. |
***
## Handling errors with the SDK
The SDKs raise a typed error on any non-2xx response (carrying the `status_code` / `statusCode` and parsed `body`) and return the parsed model on success, so you never narrow a `data | error` union by hand:
```python Python theme={null}
from hai_agents import Client
from hai_agents.core import ApiError
client = Client()
try:
session = client.sessions.create_session(
agent="h/web-surfer-flash",
messages=[{"type": "user_message", "message": "Top 3 stories on Hacker News?"}],
)
except ApiError as err:
print(err.status_code, err.body)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient, HaiAgentsError } from "hai-agents";
const client = new HaiAgentsClient();
try {
const session = await client.sessions.createSession({
body: {
agent: "h/web-surfer-flash",
messages: [{ type: "user_message", message: "Top 3 stories on Hacker News?" }],
},
});
} catch (err) {
if (err instanceof HaiAgentsError) {
console.error(err.statusCode, err.body);
}
}
```
The SDKs also retry transient errors (`408`, `429`, and all `5xx`) with backoff automatically, twice by default. Tune it with `max_retries` (Python) / `maxRetries` (TypeScript) on the client, or per call via request options. Calling the API directly? Retry those same codes with backoff, honoring `Retry-After` when present. Over-quota session creates don't need retry logic at all: they [queue](/computer-use-agents/observe-and-steer#queued-sessions) by default and start on their own as slots free up.
Do not retry `400`, `401`, `402`, `403`, `404`, or `422` errors: they signal a problem with the request itself, and the same request will fail the same way. Fix the request instead.
# Computer-Use Agents
Source: https://hub.hcompany.ai/computer-use-agents/introduction
Launch your first computer-use agent.
A **computer-use agent** sees the screen and decides what to click, type, and scroll. Use it when the work lives behind a user interface with no API to call.
Computer-Use Agents give you programmatic control over agents built on H's [Holo family of Vision Language Models](https://hcompany.ai/holo3.1). You describe a task in plain language, and H provisions the environment, runs the agent, and returns the result through a [session](/computer-use-agents/sessions/overview) your app can monitor, steer, and stop.
Agents run in a cloud browser today, with more environments on the [roadmap](#whats-next). One call starts a session:
```bash CLI theme={null}
# `hai run` creates the session and blocks until the agent answers
hai run "On Google Flights, find the cheapest direct flight from Paris (CDG) to Tokyo (NRT) this Saturday. Return the airline and the price." \
--agent h/web-surfer-flash
```
```bash cURL theme={null}
curl -X POST https://agp.eu.hcompany.ai/api/v2/sessions \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"agent": "h/web-surfer-flash", "messages": [{"type": "user_message", "message": "On Google Flights, find the cheapest direct flight from Paris (CDG) to Tokyo (NRT) this Saturday. Return the airline and the price."}]}'
```
```python Python theme={null}
# pip install hai-agents
from hai_agents import Client
client = Client()
result = client.run_session(
agent="h/web-surfer-flash",
messages=(
"On Google Flights, find the cheapest direct flight from Paris (CDG) "
"to Tokyo (NRT) this Saturday. Return the airline and the price."
),
)
print(result.answer)
```
```typescript TypeScript theme={null}
// npm install hai-agents
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const result = await client.runSession({
agent: "h/web-surfer-flash",
messages:
"On Google Flights, find the cheapest direct flight from Paris (CDG) to Tokyo " +
"(NRT) this Saturday. Return the airline and the price.",
});
console.log(result.answer);
```
That one call spins up a browser and hands it to the built-in `h/web-surfer-flash` agent. The CLI and SDK forms block until there is an answer; the raw `POST` returns the session immediately, and you follow it through its [lifecycle](/computer-use-agents/sessions/overview#lifecycle). The `h/` prefix marks H's pre-built agents; agents you create have no prefix.
You need an API key first: create one at [platform.hcompany.ai/settings/api-keys](https://platform.hcompany.ai/settings/api-keys?product=computeruseagents\&source=docs) and set it as [`HAI_API_KEY`](/computer-use-agents/quickstart#get-your-api-key) in your environment, or let `hai login` do both.
## How it fits together
The building blocks, each with a dedicated page:
| Concept | What it is |
| --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Agent](/computer-use-agents/agents/overview) | A reusable configuration: environments, skills, model, and instructions. Reference a [pre-built agent](/computer-use-agents/agents/overview#pre-built-agents) or create your own. |
| [Environment](/computer-use-agents/environments/overview) | The surface the agent sees and acts on: a cloud browser. |
| [Session](/computer-use-agents/sessions/overview) | A single run of an agent against a task, with a lifecycle you can steer. |
| [Skill](/computer-use-agents/skills/overview) | An optional, reusable instruction fragment the agent loads on demand. |
This API is in beta and still evolving. Send questions or bug reports to [support@hcompany.ai](mailto:support@hcompany.ai).
## Get started
Create your first agent session in under 5 minutes. Start here.
Install the Python or TypeScript client and skip the polling boilerplate.
See it in action: QA a live page from Claude Code, or drive a browser checkout from a plain-language prompt.
See agent, environment, and session come together at runtime.
Watch a session live or replay it afterward to debug, monitor, and follow your agents.
Plans, token and concurrency allowances, and how to manage your subscription.
## What's next
Two things are coming, with no firm dates while the API is in beta:
* **Remote desktop VMs.** A Mac, Windows, or Linux VM that H provisions in the cloud and the agent drives end to end: file management, native apps, multi-window flows. The same agent and session lifecycle you use for the browser today, on a full desktop. To drive the desktop on your own machine today, see [Local desktop](/computer-use-agents/desktop/local-control).
* **Background local desktop.** [Local desktop](/computer-use-agents/desktop/local-control) drives the screen in front of you today, so the agent shares your mouse and keyboard. Next, it will bind to individual background apps, so a session runs in its own window while you keep using your machine.
Have a request that isn't here? Send it to [support@hcompany.ai](mailto:support@hcompany.ai).
# Run agents over MCP
Source: https://hub.hcompany.ai/computer-use-agents/mcp
Run and manage H agents from any MCP host, no HTTP or SDK code.
Computer-Use Agents ship an official [Model Context Protocol](https://modelcontextprotocol.io) server, so hosts like Cursor, Claude Code, VS Code, Codex, and Hermes can run and manage H agents as tools, with no HTTP or SDK code. (Looking to search this documentation from an AI tool instead? That's the separate [docs MCP server](/docs-mcp-server).) The server is a remote streamable-HTTP endpoint at `/mcp` on your [region's host](/computer-use-agents/sdks#region), authenticated with the same H API key (`Authorization: Bearer hk-...`). Any client that can send a bearer header works; clients that require OAuth, like Claude.ai web or the ChatGPT app, aren't supported.
## Add to your editor
One click installs the `hai-agents` server, then paste your [API key](/computer-use-agents/quickstart#get-your-api-key) when prompted. The buttons target the EU region (the default):
Opens Cursor and prompts to install.
Opens VS Code and prompts to install.
On the US region, use [Add to Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=hai-agents\&config=eyJ1cmwiOiAiaHR0cHM6Ly9hZ3AuaGNvbXBhbnkuYWkvbWNwIiwgImhlYWRlcnMiOiB7IkF1dGhvcml6YXRpb24iOiAiQmVhcmVyIGhrLVlPVVJfS0VZIn19) or [Add to VS Code](vscode:mcp/install?%7B%22name%22%3A%20%22hai-agents%22%2C%20%22type%22%3A%20%22http%22%2C%20%22url%22%3A%20%22https%3A//agp.hcompany.ai/mcp%22%2C%20%22headers%22%3A%20%7B%22Authorization%22%3A%20%22Bearer%20hk-YOUR_KEY%22%7D%7D) instead. Registry-aware clients can also find it in the [MCP Registry](https://registry.modelcontextprotocol.io) as `io.github.hcompai/hai-agents`.
## Connect manually
Point your client at the `/mcp` URL for your [region](/computer-use-agents/sdks#region) and add the `Authorization` header.
```json Cursor theme={null}
// ~/.cursor/mcp.json
{
"mcpServers": {
"hai-agents": {
"url": "https://agp.eu.hcompany.ai/mcp",
"headers": { "Authorization": "Bearer hk-..." }
}
}
}
```
```bash Claude Code theme={null}
claude mcp add --scope user --transport http hai-agents https://agp.eu.hcompany.ai/mcp \
--header "Authorization: Bearer hk-..."
```
```json VS Code theme={null}
// user mcp.json (MCP: Open User Configuration)
{
"servers": {
"hai-agents": {
"type": "http",
"url": "https://agp.eu.hcompany.ai/mcp",
"headers": { "Authorization": "Bearer hk-..." }
}
}
}
```
```toml Codex theme={null}
# ~/.codex/config.toml
[mcp_servers.hai-agents]
url = "https://agp.eu.hcompany.ai/mcp"
bearer_token_env_var = "HAI_API_KEY" # export HAI_API_KEY=hk-...
```
```yaml Hermes theme={null}
# ~/.hermes/config.yaml
mcp_servers:
hai-agents:
url: https://agp.eu.hcompany.ai/mcp
headers:
Authorization: "Bearer hk-..."
```
Your API key is a secret: keep it in your local client config, never in a committed or shared workspace file.
In a network-restricted sandbox (for example NVIDIA NemoClaw / OpenShell), also allow your region's `/mcp` host in the sandbox egress policy, scoped to the process that makes the call, or the agent can't reach the server. The [computer-use-agents-demos](https://github.com/hcompai/computer-use-agents-demos/tree/main/nemoclaw) repo has a worked NemoClaw example.
## Tools
| Tool | What it does |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| `run_agent` | Start an agent on a task. Returns the answer, or a session handle to wait on if the task runs long. |
| `wait_for_session` | Long-poll a session for its answer, or fetch its current snapshot. |
| `list_agents` | List the agents you can run: your org's agents plus the public `h/` ones. Pass an agent's `name` as the `agent` for `run_agent`. |
| `send_message` | Send a follow-up message to a running session. |
| `cancel_session` | Cancel a session. |
| `share_session` | Make a session publicly viewable and return a share link. |
## Next steps
Run your first session end to end with the API or SDKs.
The typed Python and TypeScript clients and the `hai` CLI.
Configure the agents the MCP tools run, including the public `h/` ones.
Teach your assistant the H APIs with the hai-agents skill.
# Parallelize work with subagents
Source: https://hub.hcompany.ai/computer-use-agents/multi-agent
Let one manager agent split a task and delegate the pieces to specialist subagents.
Multi-agent is in **preview**. The API and behavior may change before general availability. Feedback is welcome at [support@hcompany.ai](mailto:support@hcompany.ai).
Any agent becomes a manager by listing other agents in its [`subagents`](/computer-use-agents/agents/overview#configure-an-agent). At runtime the manager breaks the task into pieces, hands each piece to a subagent, and writes the final answer once it has gathered enough. You launch a manager exactly like any other agent: one [session](/computer-use-agents/sessions/overview), one answer. The fan-out happens behind it.
Each subagent is a full [agent](/computer-use-agents/agents/overview) with its own environment, model, skills, and instructions, and runs as its own [session](/computer-use-agents/sessions/overview), isolated from its siblings. The manager runs them in parallel, so breadth that would be sequential for one agent happens at once.
Multi-agent is especially efficient for parallelizable tasks: researching a question across many sources at once, pairing a fast [text-mode](/computer-use-agents/browser/configuration#modes) searcher with a visual subagent for pages that need real clicks, or having one subagent verify what another found.
## Build a manager
A manager is just an [agent](/computer-use-agents/agents/overview) whose [`subagents`](/computer-use-agents/agents/overview#configure-an-agent) list names other agents. Create the specialists first, then reference them by name from the manager so each stays reusable and independently inspectable (inline objects also work, for one-offs). Here a research manager delegates to a fast text-mode searcher and a visual verifier.
Create a fast [text-mode](/computer-use-agents/browser/configuration#modes) searcher for broad lookups and a visual verifier for pages that need real clicks. Write each `description` as a capability statement ("Use for…"), since the manager routes on it to pick who handles what, the same way an agent [routes on a skill](/computer-use-agents/skills/overview#how-an-agent-uses-a-skill).
```bash cURL theme={null}
# Fast text-mode searcher
curl -X POST https://agp.eu.hcompany.ai/api/v2/agents \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "fast-searcher",
"description": "Searches the web quickly in text mode. Use for broad lookups and gathering candidate sources.",
"model": "holo3-1-35b-a3b",
"environments": [
{"id": "search-browser", "kind": "web", "mode": {"type": "text"}, "start_url": "https://www.bing.com"}
]
}'
# Visual verifier
curl -X POST https://agp.eu.hcompany.ai/api/v2/agents \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "visual-verifier",
"description": "Visually inspects a specific page to confirm a fact or read content behind interactions.",
"environments": ["h/browser"]
}'
```
```python Python theme={null}
from hai_agents import Client
client = Client()
client.agents.create_agent(
name="fast-searcher",
description=(
"Searches the web quickly in text mode. Use for broad lookups "
"and gathering candidate sources."
),
model="holo3-1-35b-a3b",
environments=[
{
"id": "search-browser",
"kind": "web",
"mode": {"type": "text"},
"start_url": "https://www.bing.com",
}
],
)
client.agents.create_agent(
name="visual-verifier",
description=(
"Visually inspects a specific page to confirm a fact or read "
"content behind interactions."
),
environments=["h/browser"],
)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
await client.agents.createAgent({
name: "fast-searcher",
description:
"Searches the web quickly in text mode. Use for broad lookups and gathering candidate sources.",
model: "holo3-1-35b-a3b",
environments: [
{ id: "search-browser", kind: "web", mode: { type: "text" }, startUrl: "https://www.bing.com" },
],
});
await client.agents.createAgent({
name: "visual-verifier",
description:
"Visually inspects a specific page to confirm a fact or read content behind interactions.",
environments: ["h/browser"],
});
```
Now create the manager and link the subagents by name. A manager that only delegates can omit `environments`; give it one only if it should also act on a surface itself, as this one does.
```bash cURL theme={null}
curl -X POST https://agp.eu.hcompany.ai/api/v2/agents \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "research-orchestrator",
"description": "Researches a question across sources and synthesizes a sourced answer.",
"environments": ["h/browser"],
"instructions": "Split the question into independent sub-questions, delegate each, then reconcile the findings into one sourced answer.",
"subagents": ["fast-searcher", "visual-verifier"]
}'
```
```python Python theme={null}
client.agents.create_agent(
name="research-orchestrator",
description="Researches a question across sources and synthesizes a sourced answer.",
environments=["h/browser"],
instructions=(
"Split the question into independent sub-questions, delegate each, "
"then reconcile the findings into one sourced answer."
),
subagents=["fast-searcher", "visual-verifier"],
)
```
```typescript TypeScript theme={null}
await client.agents.createAgent({
name: "research-orchestrator",
description: "Researches a question across sources and synthesizes a sourced answer.",
environments: ["h/browser"],
instructions:
"Split the question into independent sub-questions, delegate each, " +
"then reconcile the findings into one sourced answer.",
subagents: ["fast-searcher", "visual-verifier"],
});
```
Launch a session against the manager exactly like a single agent; the fan-out to subagents happens behind it. Over raw HTTP, create the session and long-poll [`changes`](/computer-use-agents/sessions/changes) until it reaches a terminal state.
```bash CLI theme={null}
hai run --agent research-orchestrator \
"Compare the starting price of the latest flagship phone from Apple, Google, and Samsung. Return one line per phone with the price, currency, and the source URL you read it from."
```
```bash cURL theme={null}
# Create the session and capture its id
SESSION_ID=$(curl -sX POST https://agp.eu.hcompany.ai/api/v2/sessions \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"agent": "research-orchestrator",
"messages": [
{"type": "user_message", "message": "Compare the starting price of the latest flagship phone from Apple, Google, and Samsung. Return one line per phone with the price, currency, and the source URL you read it from."}
]
}' | jq -r .id)
# Long-poll until the session reaches a terminal state, then print the answer.
# Advance FROM_INDEX each turn so the server waits for *new* changes instead of replaying old ones.
FROM_INDEX=0
while true; do
CHANGES=$(curl -s "https://agp.eu.hcompany.ai/api/v2/sessions/$SESSION_ID/changes?from_index=$FROM_INDEX&wait_for_seconds=25" \
-H "Authorization: Bearer $HAI_API_KEY")
[ -z "$CHANGES" ] && continue # 204 No Content: nothing new yet
FROM_INDEX=$((FROM_INDEX + $(echo "$CHANGES" | jq '.new_events | length')))
STATUS=$(echo "$CHANGES" | jq -r .status)
echo "status: $STATUS"
case "$STATUS" in
completed|failed|timed_out|interrupted)
echo "$CHANGES" | jq -r .answer
break
;;
esac
done
```
```python Python theme={null}
result = client.run_session(
agent="research-orchestrator",
messages="Compare the starting price of the latest flagship phone from Apple, Google, and Samsung. Return one line per phone with the price, currency, and the source URL you read it from.",
)
print(result.status) # "completed"
print(result.answer)
```
```typescript TypeScript theme={null}
const result = await client.runSession({
agent: "research-orchestrator",
messages:
"Compare the starting price of the latest flagship phone from Apple, Google, and " +
"Samsung. Return one line per phone with the price, currency, and the source URL you read it from.",
});
console.log(result.status); // "completed"
console.log(result.answer);
```
## How a run unfolds
The manager spawns subagents as parallel child sessions, waits for their answers, and may spawn follow-ups to fill gaps or verify findings before synthesizing the single final answer your session receives.
## What a subagent sees
A subagent works in isolation and is instructed to finish its task on its own:
* It has no access to the end user. It cannot ask questions or send messages to you; only the manager surfaces anything. Give it a self-contained task.
* The manager receives only the subagent's final answer, not its scrollback or intermediate observations. A good subagent answer carries its own data, source URLs, and caveats.
* It can delegate further. A subagent that lists its own `subagents` becomes a manager for them, nested up to 16 levels deep; a deeper chain or a cycle is rejected with `422`. Keep trees shallow well before that limit, since deep nesting multiplies sessions and cost.
## Observe and control the tree
Each subagent is a real session, so the whole tree is inspectable and steerable:
* The manager's [status](/computer-use-agents/sessions/status) lists its children in `subagent_session_ids`. [Retrieve](/computer-use-agents/sessions/retrieve) or watch any of them like a normal session.
* Filter children by their parent with `GET /sessions?parent_session_id=...`, or tag a whole run with [`group_id`](/computer-use-agents/sessions/create) and list it with `GET /sessions?group_id=...`.
* [Force an answer](/computer-use-agents/sessions/force-answer) on the manager and the signal cascades: in-flight subagents get a short grace window (about 30s) to wrap up, partial results fold into the manager's answer, and anything still unfinished is cancelled. [Cancelling](/computer-use-agents/sessions/cancel) the manager stops its subagents too, without the grace window.
# Run agents from n8n
Source: https://hub.hcompany.ai/computer-use-agents/n8n
Run Computer-Use Agents from an n8n workflow over plain HTTP.
Computer-Use Agents work in [n8n](https://n8n.io) as a standard HTTP integration. There's no custom node to install: you authenticate with a header, create a session, and either poll for the result or receive a webhook when it finishes.
In n8n, go to **Credentials → Add Credential → Header Auth** and set:
| Field | Value |
| ----- | --------------- |
| Name | `Authorization` |
| Value | `Bearer hk-...` |
Create the key at [platform.hcompany.ai/settings/api-keys](https://platform.hcompany.ai/settings/api-keys?product=computeruseagents\&source=docs). It's shown once, so copy it when you create it. A key is scoped to one organization.
Add an **HTTP Request** node:
| Setting | Value |
| ----------------- | -------------------------------------------- |
| Method | `POST` |
| URL | `https://agp.eu.hcompany.ai/api/v2/sessions` |
| Authentication | Header Auth (from step 1) |
| Body Content Type | JSON |
Body:
```json Request body theme={null}
{
"agent": "h/web-surfer-flash",
"messages": [
{"type": "user_message", "message": "Find the top 3 trending repositories on GitHub today and report their names and star counts"}
]
}
```
`h/web-surfer-flash` is a built-in agent that comes with its own browser environment, so you can run a task without setting anything else up. To run your own agent, pass its name instead, or send an inline agent object; see [Create a session](/computer-use-agents/sessions/create) for the full body.
The response returns the session `id` you'll use to poll for results:
```json Response theme={null}
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": {"status": "pending", "steps": 0},
"agent_view_url": "https://platform.hcompany.ai/agents/sessions/a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"latest_answer": null
}
```
Add a second **HTTP Request** node in a loop:
| Setting | Value |
| -------------- | ------------------------------------------------------------------ |
| Method | `GET` |
| URL | `https://agp.eu.hcompany.ai/api/v2/sessions/{{ $json.id }}/status` |
| Authentication | Header Auth (from step 1) |
Check the `status` field. The session is done when status is `completed`, `failed`, `timed_out`, or `interrupted`. Any other value (`queued`, `pending`, `running`, `paused`, `idle`, `awaiting_tool_results`) means it's still going. Use an **If** node to branch on the status, and loop the "still running" branch back through a **Wait** node before hitting the status endpoint again. Two to five seconds is a good interval; back off to ten to fifteen for tasks that run several minutes.
```json Status response theme={null}
{
"status": "running",
"error": null,
"steps": 7
}
```
The status endpoint tells you when the run is done, but it doesn't carry the answer. Once the status is `completed`, add one more **HTTP Request** node to fetch the session and read `latest_answer`:
| Setting | Value |
| -------------- | ----------------------------------------------------------- |
| Method | `GET` |
| URL | `https://agp.eu.hcompany.ai/api/v2/sessions/{{ $json.id }}` |
| Authentication | Header Auth (from step 1) |
The `latest_answer` field holds the agent's final result.
## Import the full workflow
Rather than build the four nodes by hand, copy the JSON below and paste it into an n8n canvas (**⋯ menu → Import from clipboard**, or just paste onto an empty canvas). It wires up the create, wait, poll, and read steps as a loop: the **Wait** node pauses a few seconds, **Poll Status** checks the state, and the **If** node either reads the answer (once the status is terminal) or loops back to wait.
After importing, open each HTTP Request node and select your own Header Auth credential from step 1. The credential ID baked into the export won't match your instance, so n8n shows the nodes as needing a credential until you pick yours.
```json theme={null}
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0],
"id": "cddf1db5-ac4a-4020-907c-20039726f972",
"name": "When clicking 'Execute workflow'"
},
{
"parameters": {
"method": "POST",
"url": "https://agp.eu.hcompany.ai/api/v2/sessions",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "{\n \"agent\": \"h/web-surfer-flash\",\n \"messages\": [\n {\"type\": \"user_message\", \"message\": \"Find the top 3 trending repositories on GitHub today and report their names and star counts\"}\n ]\n}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [208, 0],
"id": "afb14ac5-9686-44af-b0ff-7c6268defbb8",
"name": "Start Session",
"credentials": {
"httpHeaderAuth": {
"id": "REPLACE_WITH_YOUR_CREDENTIAL_ID",
"name": "Header Auth account"
}
}
},
{
"parameters": {
"amount": 3,
"unit": "seconds"
},
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [416, 0],
"id": "a1111111-1111-4111-8111-111111111111",
"name": "Wait",
"webhookId": "b2222222-2222-4222-8222-222222222222"
},
{
"parameters": {
"url": "=https://agp.eu.hcompany.ai/api/v2/sessions/{{ $('Start Session').item.json.id }}/status",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [624, 0],
"id": "cdff9632-065d-41a0-b71f-563d70c2eddf",
"name": "Poll Status",
"credentials": {
"httpHeaderAuth": {
"id": "REPLACE_WITH_YOUR_CREDENTIAL_ID",
"name": "Header Auth account"
}
}
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "c3333333-3333-4333-8333-333333333333",
"leftValue": "={{ ['completed','failed','timed_out','interrupted'].includes($json.status) }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [832, 0],
"id": "d4444444-4444-4444-8444-444444444444",
"name": "Session finished?"
},
{
"parameters": {
"url": "=https://agp.eu.hcompany.ai/api/v2/sessions/{{ $('Start Session').item.json.id }}",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [1040, -96],
"id": "72eccd6a-4d0e-4f55-9809-ba7708d8760b",
"name": "Read Answer",
"credentials": {
"httpHeaderAuth": {
"id": "REPLACE_WITH_YOUR_CREDENTIAL_ID",
"name": "Header Auth account"
}
}
}
],
"connections": {
"When clicking 'Execute workflow'": {
"main": [[{ "node": "Start Session", "type": "main", "index": 0 }]]
},
"Start Session": {
"main": [[{ "node": "Wait", "type": "main", "index": 0 }]]
},
"Wait": {
"main": [[{ "node": "Poll Status", "type": "main", "index": 0 }]]
},
"Poll Status": {
"main": [[{ "node": "Session finished?", "type": "main", "index": 0 }]]
},
"Session finished?": {
"main": [
[{ "node": "Read Answer", "type": "main", "index": 0 }],
[{ "node": "Wait", "type": "main", "index": 0 }]
]
}
},
"pinData": {},
"meta": {}
}
```
The loop runs until the session reaches a terminal state; add a counter if you want a hard cap on the number of polls.
## Event-driven alternative
Instead of polling, use [Webhooks](/computer-use-agents/webhooks/overview) to get a callback when a session changes status. Add a **Webhook** trigger node in n8n, copy its Production URL, then register that URL for your organization with the [Create webhook](/computer-use-agents/webhooks/create) API. Subscribe to the events you care about, like `session.completed` and `session.failed`, or to `session.status_updated` for every transition (see the [event catalog](/computer-use-agents/webhooks/events)). n8n then receives a `POST` whenever a matching event fires, with `session_id`, `status`, and `previous_status` in the payload.
Deliveries are signed and retried with backoff, so the same event can arrive more than once. Verify the signature (see [Verifying deliveries](/computer-use-agents/webhooks/overview#verifying-deliveries)), skip event ids you've already processed, and treat the webhook as a trigger: fetch the session for the authoritative state.
The same pattern works with any tool that can make HTTP requests, such as Make, Zapier, Pipedream, or your own orchestrator.
## Next steps
The full session request body and options.
States, step count, and the polling pattern.
Signed callbacks on status changes, and how to verify them.
The typed Python and TypeScript clients and the hai CLI.
# Watch and steer sessions
Source: https://hub.hcompany.ai/computer-use-agents/observe-and-steer
Watch a run, intervene while it works, keep the conversation going, and read how it ended.
Every session is visible in **Agent View** on the [H Platform](https://platform.hcompany.ai/?product=computeruseagents\&source=docs), and its `agent_view_url` links straight there. Open a running session to see what the agent sees while it works, the quickest way to prompt-tune, debug an unexpected detour, or confirm a run hasn't stalled; once it terminates, scrub through the full trajectory (every observation, action, and message) to audit what happened. The view reads the same [`events`](/computer-use-agents/sessions/events) stream your code does, so anything the API exposes is reflected in the UI.
Everything on this page is addressed to the session's `id` and works the same whether the agent runs alone or [delegates to subagents](/computer-use-agents/multi-agent).
In the Python and TypeScript SDKs, starting a session returns a lightweight handle bound to that `id`, and the snippets below read and steer through it. Every operation is also available as a direct client call or a raw HTTP request, as each reference page shows.
If your organization is at its concurrency limit, a new session first sits in [`queued`](#queued-sessions) and starts on its own once a slot frees up.
## Watch a run
There are three ways to read a session, from cheapest to most complete:
* [`status`](/computer-use-agents/sessions/status) returns a small snapshot of the current [state](/computer-use-agents/sessions/overview#lifecycle), step count, and token usage. Poll it on an interval as a health check or to detect a terminal state.
* [`changes`](/computer-use-agents/sessions/changes) long-polls from an event index: the call blocks until something new happens, then returns the new events and the final `answer` once it lands. Use this while a run is active.
* [`events`](/computer-use-agents/sessions/events) is the complete, paginated record of everything the agent observed and did. Page through it to replay or audit a run after the fact.
```python Python theme={null}
from hai_agents import Client
client = Client()
session = client.start_session(
agent="h/web-surfer-flash",
messages=[{"type": "user_message", "message": "Find the top story on Hacker News"}],
)
session.status() # cheap liveness snapshot
session.changes(from_index=0) # new events + final answer, long-polled
session.get() # the full Session resource
result = session.wait_for_completion() # block until terminal, then read the answer
print(result.status, result.answer)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const session = await client.startSession({
agent: "h/web-surfer-flash",
messages: [{ type: "user_message", message: "Find the top story on Hacker News" }],
});
await session.status(); // cheap liveness snapshot
await session.changes({ fromIndex: 0 }); // new events + final answer, long-polled
await session.get(); // the full Session resource
const result = await session.waitForCompletion(); // block until terminal, then read the answer
console.log(result.status, result.answer);
```
### Stream events as they arrive
[`changes`](/computer-use-agents/sessions/changes) is a single long-poll: one request that returns the events available past an index. To consume a whole run as a live feed, the SDK handle exposes `stream()`, an iterator that runs the long-poll loop for you and yields each [event](/computer-use-agents/sessions/events) in order until the session settles. It resumes `from_index` and drops the `204` no-change responses automatically, so you only see events.
By default it stops as soon as the session settles (a [terminal state](/computer-use-agents/sessions/overview#lifecycle), or `idle` awaiting your next message); pass `until="terminal"` to keep the feed open across the idle turns of an [interactive session](#hold-an-interactive-conversation). `stream()` is a read-only view and does not answer tool calls: for runs that use [custom tools](/computer-use-agents/custom-tools), use `wait_for_completion` / `run_session`, which run the tools for you.
```python Python theme={null}
for event in session.stream():
print(event.type)
# With the async client, iterate the same handle with `async for`.
```
```typescript TypeScript theme={null}
for await (const event of session.stream()) {
console.log(event.type);
}
```
## Steer a running agent
As long as the session is not in a [terminal state](/computer-use-agents/sessions/overview#lifecycle), you can intervene:
* Send a message to add context or redirect the agent mid-run. The message is picked up on the next step; a message to an `idle` session also wakes it. See [Send a message](/computer-use-agents/sessions/send-messages).
* Pause and resume to halt the agent with its state preserved (for review or cost control), then continue. Sending a message auto-resumes a paused session. See [Pause](/computer-use-agents/sessions/pause) and [Resume](/computer-use-agents/sessions/resume).
* Force an answer to tell the agent to stop exploring and commit to a final answer from what it has so far. See [Force an answer](/computer-use-agents/sessions/force-answer).
* Cancel to stop the session for good; it ends in `interrupted`. See [Cancel](/computer-use-agents/sessions/cancel).
A blocking run-and-wait call never surfaces the session mid-run: start the session and keep its handle when you need to read or intervene while it works.
Sending a steering message is the most common intervention:
```bash CLI theme={null}
hai sessions send "$SESSION_ID" "Only consider results from the last 24 hours"
```
```bash cURL theme={null}
curl -X POST "https://agp.eu.hcompany.ai/api/v2/sessions/$SESSION_ID/messages" \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"type": "user_message", "message": "Only consider results from the last 24 hours"}'
```
```python Python theme={null}
session.send_message({"type": "user_message", "message": "Only consider results from the last 24 hours"})
```
```typescript TypeScript theme={null}
await session.sendMessage({ type: "user_message", message: "Only consider results from the last 24 hours" });
```
The other interventions follow the same shape:
```bash cURL theme={null}
curl -X POST "https://agp.eu.hcompany.ai/api/v2/sessions/$SESSION_ID/pause" -H "Authorization: Bearer $HAI_API_KEY" # halt, state preserved
curl -X POST "https://agp.eu.hcompany.ai/api/v2/sessions/$SESSION_ID/resume" -H "Authorization: Bearer $HAI_API_KEY" # continue where it left off
curl -X POST "https://agp.eu.hcompany.ai/api/v2/sessions/$SESSION_ID/force_answer" -H "Authorization: Bearer $HAI_API_KEY" # stop exploring and commit to a final answer
curl -X DELETE "https://agp.eu.hcompany.ai/api/v2/sessions/$SESSION_ID" -H "Authorization: Bearer $HAI_API_KEY" # stop for good; ends in `interrupted`
```
```python Python theme={null}
session.pause() # halt, state preserved
session.resume() # continue where it left off
session.force_answer() # stop exploring and commit to a final answer
session.cancel() # stop for good; ends in `interrupted`
```
```typescript TypeScript theme={null}
await session.pause();
await session.resume();
await session.forceAnswer();
await session.cancel();
```
## Hold an interactive conversation
By default a session ends as soon as the agent answers. Set [`idle_timeout_s`](/computer-use-agents/sessions/create) when you create it to keep it open: after each answer the session enters [`idle`](/computer-use-agents/sessions/overview#lifecycle) and waits that long for your next message before terminating. One session becomes a multi-turn conversation that keeps its full context and environment state across turns.
```bash cURL theme={null}
# Open an interactive session that stays alive for 10 minutes between turns.
SESSION_ID=$(curl -sX POST https://agp.eu.hcompany.ai/api/v2/sessions \
-H "Authorization: Bearer $HAI_API_KEY" -H "Content-Type: application/json" \
-d '{
"agent": "h/web-surfer-flash",
"idle_timeout_s": 600,
"messages": [{"type": "user_message", "message": "Find the top story on Hacker News"}]
}' | jq -r .id)
# After it answers and goes idle, ask a follow-up in the same context.
curl -X POST "https://agp.eu.hcompany.ai/api/v2/sessions/$SESSION_ID/messages" \
-H "Authorization: Bearer $HAI_API_KEY" -H "Content-Type: application/json" \
-d '{"type": "user_message", "message": "Now open its comments and summarize the discussion"}'
```
```python Python theme={null}
session = client.start_session(
agent="h/web-surfer-flash",
idle_timeout_s=600,
messages=[{"type": "user_message", "message": "Find the top story on Hacker News"}],
)
# After it answers and goes idle, ask a follow-up in the same context.
session.send_message({"type": "user_message", "message": "Now open its comments and summarize the discussion"})
```
```typescript TypeScript theme={null}
const session = await client.startSession({
agent: "h/web-surfer-flash",
idleTimeoutS: 600,
messages: [{ type: "user_message", message: "Find the top story on Hacker News" }],
});
// After it answers and goes idle, ask a follow-up in the same context.
await session.sendMessage({ type: "user_message", message: "Now open its comments and summarize the discussion" });
```
Watch the session's [`status`](/computer-use-agents/sessions/status) flip to `idle` between turns; it terminates once a turn goes unanswered for `idle_timeout_s`.
## Queued sessions
Creating a session while your organization is at its [concurrency limit](/computer-use-agents/plans-and-limits#concurrent-sessions) doesn't fail: the create returns `201` with status `queued`, and the session starts automatically, oldest first, as running sessions finish. A queued session goes through the normal [lifecycle](/computer-use-agents/sessions/overview#lifecycle) (`queued` → `pending` → `running` → terminal) and fires a [webhook](/computer-use-agents/webhooks/overview) on every transition. If you prefer an immediate error, set [`queue: false`](/computer-use-agents/sessions/create) on the create body to get a `429` instead.
Messages sent to a queued session are buffered and delivered when it starts; pause and resume are not available until then, and [cancelling](/computer-use-agents/sessions/cancel) dequeues it immediately. The queue holds up to 1000 sessions per organization; beyond that, creates return `429` again. [Child sessions](/computer-use-agents/multi-agent) are the one exception: a create carrying `parent_session_id` fails fast with `429` at capacity, because a child queued behind its own running parent could wait forever.
Queueing makes batches simple: fire all your tasks at once and let the platform pace them to your quota.
```python Python theme={null}
from hai_agents import Client
client = Client()
sessions = [
client.sessions.create_session(
agent="h/web-surfer-flash",
messages=f"Check price for {product}",
)
for product in products
]
# first ones run immediately, the rest are queued; collect results via webhooks
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const sessions = await Promise.all(
products.map((product) =>
client.sessions.createSession({
body: {
agent: "h/web-surfer-flash",
messages: `Check price for ${product}`,
},
})
)
);
// first ones run immediately, the rest are queued; collect results via webhooks
```
## Read how the run ended
When the session settles, its status carries machine-readable signals about how the run went, so your code can branch without parsing prose. They appear on [`status`](/computer-use-agents/sessions/status), on the [Session object](/computer-use-agents/sessions/overview), and on [`changes`](/computer-use-agents/sessions/changes).
### Outcomes
A session can end `completed` and still not have done what you asked, so the agent reports its own assessment alongside the final answer:
| `outcome` | Meaning |
| ------------ | ------------------------------------------------------------------------------------------------- |
| `success` | The task was fully accomplished. |
| `partial` | Some of the task was accomplished, but not all of it. |
| `infeasible` | The task cannot be accomplished as specified, for example when the requested item does not exist. |
| `blocked` | An external obstacle stopped progress: a login wall, a captcha, or missing permissions. |
The outcome is the agent's self-assessment, not verified ground truth. It is still a strong routing signal: `blocked` usually means a human needs to connect an account or a [vault](/computer-use-agents/vaults/overview), and `infeasible` usually means retrying is pointless. For high-stakes flows, validate the answer itself. `outcome` is `null` when the agent ended without reporting one.
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const result = await client.runSession({
agent: "h/web-surfer-flash",
messages: "Cancel my Acme Co subscription",
});
switch (result.outcome) {
case "success":
return result.answer;
case "blocked":
// needs credentials or a human in the loop
return escalate(result);
case "infeasible":
return giveUp(result);
default:
// "partial", null: inspect before trusting
return review(result);
}
```
### Error codes
When a session ends `failed` or `timed_out`, its status carries an `error_code` from a small fixed taxonomy and an `error` message matching the code. The code tells you whether a retry makes sense:
| `error_code` | Meaning | Retry? |
| ------------------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------- |
| `environment_error` | The session's environment failed to provision or crashed. | Yes, as is. Nothing about your request was wrong. |
| `no_answer` | The agent ran out of budget (`max_steps` / `max_time_s`) or stopped without producing an answer. | Yes, with a higher budget or a more focused task. |
| `answer_validation` | The agent answered, but every attempt failed to match the agent's `answer_format`. | Maybe. Simplify the schema or loosen required fields. |
| `timeout` | The session exceeded its maximum allowed time (`status` is `timed_out`). | Yes, with a higher `max_time_s` or a smaller task. |
| `internal` | An unexpected platform-side error. | Yes, once; if it persists, contact support. |
`error_code` is `null` unless the status is `failed` or `timed_out`. New codes may be added over time, so treat unknown values like `internal`. The `error` message is a stable template derived from the code, never raw internals, so branch on `error_code` and log the message.
```python Python theme={null}
from hai_agents import Client
client = Client()
result = client.run_session(
agent="h/web-surfer-flash",
messages="Find the current price of the Framework 13 laptop",
)
if result.status in ("failed", "timed_out"):
if result.error_code == "environment_error":
result = client.run_session(...) # transient: retry as is
elif result.error_code in ("no_answer", "timeout"):
... # raise the budget or narrow the task before retrying
else:
raise RuntimeError(f"Session failed: {result.error} ({result.error_code})")
```
These signals describe how a run ended. For HTTP-level errors on the API calls themselves, see [Errors](/computer-use-agents/errors).
# Plans and limits
Source: https://hub.hcompany.ai/computer-use-agents/plans-and-limits
Plans, token and concurrency allowances, and how to manage your subscription.
Your organization runs on a **plan** that sets two allowances: how many **tokens** you can use per billing period, and how many **sessions** you can run at the same time. Usage is tracked per organization, both allowances are readable from the API at any time, and there are no request-rate limits.
## Plans
| | **Free** | **Developer** |
| ------------------------- | ------------------------------------ | -------------------------------------------- |
| Price | \$0 | \$29 / month |
| Tokens per billing period | 15,000,000 | 65,000,000 |
| Concurrent sessions | 3 | 10 |
| Resets | Monthly, on your signup day-of-month | Monthly, on your subscription's billing date |
Token and concurrency allowances are subject to change, so read the live values from the API (below) rather than hard-coding the numbers in your integration.
## Token usage
Tokens are consumed by the model as your agents run, and they accrue against your plan's per-period allowance. When the allowance runs out, creating a session (or messaging a finished one, which restarts it) fails with `402 Payment Required`. The error's `detail` carries your `limit`, `used`, and the `window_end` when the budget resets; see [Errors](/computer-use-agents/errors).
Check your current token usage with `GET /api/v2/quota/tokens`:
```bash cURL theme={null}
curl https://agp.eu.hcompany.ai/api/v2/quota/tokens \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
tokens = client.quota.get_token_quota()
print(tokens.remaining)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const tokens = await client.quota.getTokenQuota();
console.log(tokens.remaining);
```
```json Response theme={null}
{
"limit": 15000000,
"used": 4200000,
"remaining": 10800000,
"window_start": "2026-06-01T00:00:00Z",
"window_end": "2026-07-01T00:00:00Z"
}
```
| Field | Type | Description |
| -------------- | --------------- | -------------------------------------------------------------------------------------- |
| `limit` | integer \| null | Tokens allowed this period. `null` means unlimited. |
| `used` | integer \| null | Tokens used so far this period. `null` if the usage figure is momentarily unavailable. |
| `remaining` | integer \| null | Tokens left (`max(limit - used, 0)`). |
| `window_start` | string | Start of the current billing period (UTC). |
| `window_end` | string \| null | End of the current billing period (UTC). |
## Concurrent sessions
Separately from tokens, your plan caps how many sessions can run at once. A session holds a slot while it is in a non-terminal state (`pending`, `running`, `awaiting_tool_results`, `paused`, or `idle`) and frees it as soon as it reaches a terminal state, including when you [cancel](/computer-use-agents/sessions/cancel) it. [`queued`](/computer-use-agents/observe-and-steer#queued-sessions) sessions hold no slot and never count against your quota.
`GET /api/v2/sessions/quota` returns your current concurrency usage:
```bash cURL theme={null}
curl https://agp.eu.hcompany.ai/api/v2/sessions/quota \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
quota = client.sessions.get_session_quota()
print(quota.available)
```
```typescript TypeScript theme={null}
const quota = await client.sessions.getSessionQuota();
console.log(quota.available);
```
```json Response theme={null}
{
"scope": "user",
"limit": 10,
"active": 3,
"available": 7
}
```
See [Get quota](/computer-use-agents/sessions/quota) for the field-by-field reference.
Creating a session while at your limit doesn't fail: the create is accepted with status [`queued`](/computer-use-agents/observe-and-steer#queued-sessions) and the session starts automatically when a slot frees up. If you prefer an immediate error, set [`queue: false`](/computer-use-agents/sessions/create) on the create body to get a `429 Too Many Requests` instead; the SDKs retry `429` with backoff automatically, see [Errors](/computer-use-agents/errors#handling-errors-with-the-sdk).
## Managing your subscription
Start a Developer subscription from the billing page in the [platform dashboard](https://platform.hcompany.ai/settings/billing?product=computeruseagents\&plan=developer\&source=docs); checkout is card-only. Manage your card and download invoices through the Stripe billing portal, linked from the same page. Cancelling stops the renewal: your Developer allowances stay active until the end of the current billing period, after which the organization reverts to the Free plan.
## Need higher limits?
For higher token or concurrency allowances, or an Enterprise plan, contact us at [support@hcompany.ai](mailto:support@hcompany.ai).
# Quickstart
Source: https://hub.hcompany.ai/computer-use-agents/quickstart
Create your first agent session in under 5 minutes.
Set up a browser environment, create an agent, run it on a task, and read its answer. All you need is an API key. To skip the setup and just watch a task run, one call to the built-in `h/web-surfer-flash` agent does it (see the [introduction](/computer-use-agents/introduction)).
Install the `hai-agents` client (and CLI). Pick a language below; it applies to every code block on this page.
```bash CLI theme={null}
pip install "hai-agents[cli]"
```
```bash cURL theme={null}
# no install needed, the API is plain HTTP
```
```bash Python theme={null}
pip install hai-agents
```
```bash TypeScript theme={null}
npm install hai-agents
```
Create a key at [platform.hcompany.ai/settings/api-keys](https://platform.hcompany.ai/settings/api-keys?product=computeruseagents\&source=docs). It's shown only once, so store it securely and keep it server-side. The key is scoped to your **organization**: everything you create with it is private to that org.
Set it as `HAI_API_KEY` in your environment. Raw HTTP sends it as a bearer token in the `Authorization` header; the CLI and SDKs pick it up automatically.
```bash CLI theme={null}
hai login # browser sign-in; creates and stores your key in ~/.config/hai/.env
```
```bash cURL theme={null}
export HAI_API_KEY="hk-..."
```
```python Python theme={null}
from hai_agents import Client
client = Client()
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
```
An [environment](/computer-use-agents/environments/overview) is what your agent sees and acts on. Register a web browser in [`visual` mode](/computer-use-agents/browser/configuration#modes), where the agent works from screenshots and clicks by coordinates, and give it an `id` the agent will reference.
```bash cURL theme={null}
curl -X POST https://agp.eu.hcompany.ai/api/v2/environments \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"id": "visual-browser",
"kind": "web",
"mode": {"type": "visual", "width": 1200, "height": 1200},
"start_url": "https://www.google.com/"
}'
```
```python Python theme={null}
client.environments.create_environment(
id="visual-browser",
kind="web",
mode={"type": "visual", "width": 1200, "height": 1200},
start_url="https://www.google.com/",
)
```
```typescript TypeScript theme={null}
await client.environments.createEnvironment({
kind: "web",
id: "visual-browser",
mode: { type: "visual", width: 1200, height: 1200 },
startUrl: "https://www.google.com/",
});
```
Create an agent that references the environment by `id`, so you can reuse it across sessions. Agents you create have no prefix; H's pre-built agents and environments use the reserved `h/` namespace (like `h/web-surfer-flash` and `h/browser`). The optional `instructions` shape how the agent behaves on every run:
```bash cURL theme={null}
curl -X POST https://agp.eu.hcompany.ai/api/v2/agents \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "web-navigator",
"description": "Navigates and operates interactive websites to carry out a task end to end.",
"instructions": "Ground every claim in what you actually see; never invent values you have not observed. Check the page changed as expected after each action, operate the controls a task needs (filters, dropdowns, date pickers), and finish the whole task. If something is blocked or unavailable, say so plainly instead of guessing.",
"environments": ["visual-browser"]
}'
```
```python Python theme={null}
client.agents.create_agent(
name="web-navigator",
description="Navigates and operates interactive websites to carry out a task end to end.",
instructions=(
"Ground every claim in what you actually see; never invent values you have not "
"observed. Check the page changed as expected after each action, operate the "
"controls a task needs (filters, dropdowns, date pickers), and finish the whole "
"task. If something is blocked or unavailable, say so plainly instead of guessing."
),
environments=["visual-browser"],
)
```
```typescript TypeScript theme={null}
await client.agents.createAgent({
name: "web-navigator",
description: "Navigates and operates interactive websites to carry out a task end to end.",
instructions:
"Ground every claim in what you actually see; never invent values you have not " +
"observed. Check the page changed as expected after each action, operate the " +
"controls a task needs (filters, dropdowns, date pickers), and finish the whole " +
"task. If something is blocked or unavailable, say so plainly instead of guessing.",
environments: ["visual-browser"],
});
```
Launch a session against `web-navigator` and describe the task in plain language. Google Flights is a good test: its date picker, filters, and result cards only respond to real clicks, so the agent has to drive the page.
The CLI and SDK calls below create the session and block until the final `answer`. Over raw HTTP there's no single blocking call, so you create the session, long-poll [`changes`](/computer-use-agents/sessions/changes) until it reaches a terminal state, then read the settled answer off the [session snapshot](/computer-use-agents/sessions/retrieve).
```bash CLI theme={null}
hai run --agent web-navigator \
"On Google Flights, search a round trip from Paris (CDG) to New York (JFK): set departure to the first Monday of next month and the return one week later using the date picker, filter to nonstop flights, then open the cheapest result. Report the airline, total price, and departure time shown on its details."
```
```bash cURL theme={null}
# Create the session and capture its id
SESSION_ID=$(curl -sX POST https://agp.eu.hcompany.ai/api/v2/sessions \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"agent": "web-navigator",
"messages": [
{"type": "user_message", "message": "On Google Flights, search a round trip from Paris (CDG) to New York (JFK): set departure to the first Monday of next month and the return one week later using the date picker, filter to nonstop flights, then open the cheapest result. Report the airline, total price, and departure time shown on its details."}
]
}' | jq -r .id)
# Long-poll until the session reaches a terminal state.
# Advance FROM_INDEX each turn so the server waits for *new* changes instead of replaying old ones.
FROM_INDEX=0
while true; do
CHANGES=$(curl -s "https://agp.eu.hcompany.ai/api/v2/sessions/$SESSION_ID/changes?from_index=$FROM_INDEX&wait_for_seconds=25" \
-H "Authorization: Bearer $HAI_API_KEY")
[ -z "$CHANGES" ] && continue # 204 No Content: nothing new yet
FROM_INDEX=$((FROM_INDEX + $(echo "$CHANGES" | jq '.new_events | length')))
STATUS=$(echo "$CHANGES" | jq -r .status)
echo "status: $STATUS"
case "$STATUS" in
completed|failed|timed_out|interrupted) break ;;
esac
done
# The answer rides the page carrying the final events, which may trail the
# status flip; the session snapshot has the settled value.
curl -s "https://agp.eu.hcompany.ai/api/v2/sessions/$SESSION_ID" \
-H "Authorization: Bearer $HAI_API_KEY" | jq -r .latest_answer
```
```python Python theme={null}
result = client.run_session(
agent="web-navigator",
messages="On Google Flights, search a round trip from Paris (CDG) to New York (JFK): set departure to the first Monday of next month and the return one week later using the date picker, filter to nonstop flights, then open the cheapest result. Report the airline, total price, and departure time shown on its details.",
)
print(result.status) # "completed"
print(result.answer)
```
```typescript TypeScript theme={null}
const result = await client.runSession({
agent: "web-navigator",
messages:
"On Google Flights, search a round trip from Paris (CDG) to New York (JFK): set " +
"departure to the first Monday of next month and the return one week later using " +
"the date picker, filter to nonstop flights, then open the cheapest result. Report " +
"the airline, total price, and departure time shown on its details.",
});
console.log(result.status); // "completed"
console.log(result.answer);
```
Need live progress instead of one blocking call? Poll [`status`](/computer-use-agents/sessions/status) for state and step count, or [long-poll `changes`](/computer-use-agents/sessions/changes) to stream events as they happen.
Open the [H Platform](https://platform.hcompany.ai/?product=computeruseagents\&source=docs) to see your sessions: watch a running one step by step, or scrub a finished run to replay the full trajectory. See [Agent View](/computer-use-agents/observe-and-steer) for details.
***
## Next steps
Reusable configurations: built-in agents and how to create your own.
The surfaces your agent perceives and acts on. Browser today; more in [What's next](/computer-use-agents/introduction#whats-next).
Reusable instruction fragments you can attach to agents.
The session lifecycle and how to interact with a running agent.
# Create a schedule
Source: https://hub.hcompany.ai/computer-use-agents/schedules/create
POST /api/v2/schedules
Register a cron schedule that starts a session on each fire.
Registers a [schedule](/computer-use-agents/schedules/overview) for your organization. The `session_request` template is validated and resolved at creation time (the agent must exist), then re-resolved on every fire.
**Returns** `201` with the created [schedule object](/computer-use-agents/schedules/retrieve).
***
## Request body
Display name for the schedule (max 255 characters).
When the schedule fires:
* `expression` (string): Five-field cron expression, e.g. `"0 9 * * 1-5"`.
* `timezone` (string): IANA timezone the expression is evaluated in, e.g. `"Europe/Paris"`.
* `type` (string, optional): `"cron"`, the default and only variant today.
The expression may not fire more often than once every 5 minutes.
Template used to create each scheduled session, in the same shape as the [Create session](/computer-use-agents/sessions/create) body. Must contain at least one initial message and may not set `parent_session_id`. Defaults to `max_time_s: 3600` when the template does not set it.
Optional description (max 255 characters).
***
## Examples
```bash cURL theme={null}
curl -X POST https://agp.eu.hcompany.ai/api/v2/schedules \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "morning-market-scan",
"description": "Weekday scan of new Paris listings",
"timing": {"expression": "0 9 * * 1-5", "timezone": "Europe/Paris"},
"session_request": {
"agent": "h/web-surfer-flash",
"messages": [
{"type": "user_message", "message": "Scan new apartment listings in Paris 11e and summarize the top five"}
]
}
}'
```
```python Python theme={null}
from hai_agents import Client
client = Client()
schedule = client.schedules.create_schedule(
name="morning-market-scan",
description="Weekday scan of new Paris listings",
timing={"expression": "0 9 * * 1-5", "timezone": "Europe/Paris"},
session_request={
"agent": "h/web-surfer-flash",
"messages": [
{"type": "user_message", "message": "Scan new apartment listings in Paris 11e and summarize the top five"}
],
},
)
print(schedule.id, schedule.next_run_times[0])
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const schedule = await client.schedules.createSchedule({
name: "morning-market-scan",
description: "Weekday scan of new Paris listings",
timing: { expression: "0 9 * * 1-5", timezone: "Europe/Paris" },
sessionRequest: {
agent: "h/web-surfer-flash",
messages: [
{ type: "user_message", message: "Scan new apartment listings in Paris 11e and summarize the top five" },
],
},
});
console.log(schedule.id, schedule.nextRunTimes[0]);
```
```json Response theme={null}
{
"id": "9f8e7d6c-5b4a-4c3d-8e2f-1a0b9c8d7e6f",
"name": "morning-market-scan",
"description": "Weekday scan of new Paris listings",
"timing": {"type": "cron", "expression": "0 9 * * 1-5", "timezone": "Europe/Paris"},
"session_request": {
"agent": "h/web-surfer-flash",
"messages": [
{"type": "user_message", "message": "Scan new apartment listings in Paris 11e and summarize the top five"}
],
"max_time_s": 3600
},
"paused": false,
"pause_note": null,
"next_run_times": [
"2026-07-06T07:00:00Z",
"2026-07-07T07:00:00Z",
"2026-07-08T07:00:00Z",
"2026-07-09T07:00:00Z",
"2026-07-10T07:00:00Z"
],
"last_run_at": null,
"created_at": "2026-07-04T12:00:00Z",
"updated_at": "2026-07-04T12:00:00Z"
}
```
***
## Errors
| Status | Cause |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------- |
| `400` | Organization schedule limit reached (20), or the expression fires more often than once every 5 minutes. |
| `404` | The agent referenced by `session_request` doesn't exist or isn't visible to you. |
| `422` | Body failed validation: invalid cron expression, unknown timezone, template without messages, or template setting `parent_session_id`. |
# Delete a schedule
Source: https://hub.hcompany.ai/computer-use-agents/schedules/delete
DELETE /api/v2/schedules/{schedule_id}
Remove a schedule and stop future fires.
Deletes the schedule and its run history. Future fires stop; sessions already created by past fires keep running.
**Returns** `204` with no body.
***
## Path parameters
The schedule's id (UUID).
***
## Examples
```bash cURL theme={null}
curl -X DELETE "https://agp.eu.hcompany.ai/api/v2/schedules/9f8e7d6c-5b4a-4c3d-8e2f-1a0b9c8d7e6f" \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
client.schedules.delete_schedule("9f8e7d6c-5b4a-4c3d-8e2f-1a0b9c8d7e6f")
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
await client.schedules.deleteSchedule({ scheduleId: "9f8e7d6c-5b4a-4c3d-8e2f-1a0b9c8d7e6f" });
```
***
## Errors
| Status | Cause |
| ------ | ---------------------------------------------- |
| `404` | No schedule with this id in your organization. |
# List schedules
Source: https://hub.hcompany.ai/computer-use-agents/schedules/list
GET /api/v2/schedules
Browse your organization's schedules.
Returns a paginated list of your organization's schedules.
**Returns** a paginated list of [schedule objects](/computer-use-agents/schedules/retrieve).
***
## Query parameters
Page number (1-based).
Items per page. Maximum: `1000`.
Sort order. Options: `created_at`, `-created_at`.
***
## Examples
```bash cURL theme={null}
curl "https://agp.eu.hcompany.ai/api/v2/schedules" \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
page = client.schedules.list_schedules()
for schedule in page.items:
print(schedule.id, schedule.name, schedule.paused)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const page = await client.schedules.listSchedules();
for (const schedule of page.items) {
console.log(schedule.id, schedule.name, schedule.paused);
}
```
```json Response theme={null}
{
"items": [
{
"id": "9f8e7d6c-5b4a-4c3d-8e2f-1a0b9c8d7e6f",
"name": "morning-market-scan",
"description": "Weekday scan of new Paris listings",
"timing": {"type": "cron", "expression": "0 9 * * 1-5", "timezone": "Europe/Paris"},
"session_request": {
"agent": "h/web-surfer-flash",
"messages": [
{"type": "user_message", "message": "Scan new apartment listings in Paris 11e and summarize the top five"}
],
"max_time_s": 3600
},
"paused": false,
"pause_note": null,
"next_run_times": [
"2026-07-06T07:00:00Z",
"2026-07-07T07:00:00Z",
"2026-07-08T07:00:00Z",
"2026-07-09T07:00:00Z",
"2026-07-10T07:00:00Z"
],
"last_run_at": "2026-07-03T07:00:00Z",
"created_at": "2026-06-20T12:00:00Z",
"updated_at": "2026-07-03T07:00:00Z"
}
],
"page": 1,
"total": 1
}
```
# Run sessions on a schedule
Source: https://hub.hcompany.ai/computer-use-agents/schedules/overview
Run a session on a recurring cron cadence.
A schedule creates a [session](/computer-use-agents/sessions/overview) on a recurring cadence: a five-field cron expression evaluated in an IANA timezone, paired with a session template that is re-resolved on every fire. Use it for recurring work like a daily scrape or an hourly check.
Manage schedules with the [CRUD API](/computer-use-agents/schedules/create). Each one has a `name`, a `timing`, and a `session_request` template:
```bash cURL theme={null}
curl -X POST https://agp.eu.hcompany.ai/api/v2/schedules \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "morning-market-scan",
"timing": {"expression": "0 9 * * 1-5", "timezone": "Europe/Paris"},
"session_request": {
"agent": "h/web-surfer-flash",
"messages": [
{"type": "user_message", "message": "Scan new apartment listings in Paris 11e and summarize the top five"}
]
}
}'
```
```python Python theme={null}
from hai_agents import Client
client = Client()
schedule = client.schedules.create_schedule(
name="morning-market-scan",
timing={"expression": "0 9 * * 1-5", "timezone": "Europe/Paris"},
session_request={
"agent": "h/web-surfer-flash",
"messages": [
{"type": "user_message", "message": "Scan new apartment listings in Paris 11e and summarize the top five"}
],
},
)
print(schedule.id, schedule.next_run_times[0])
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const schedule = await client.schedules.createSchedule({
name: "morning-market-scan",
timing: { expression: "0 9 * * 1-5", timezone: "Europe/Paris" },
sessionRequest: {
agent: "h/web-surfer-flash",
messages: [
{ type: "user_message", message: "Scan new apartment listings in Paris 11e and summarize the top five" },
],
},
});
console.log(schedule.id, schedule.nextRunTimes[0]);
```
## Timing
```json Timing field theme={null}
"timing": {
"type": "cron",
"expression": "0 9 * * 1-5",
"timezone": "Europe/Paris"
}
```
The expression is standard five-field cron (`minute hour day-of-month month day-of-week`), evaluated in the given timezone, so `0 9 * * 1-5` fires at 09:00 Paris time every weekday, across daylight-saving changes. An expression may not fire more often than once every 5 minutes. The `type` tag is optional on requests; `"cron"` is the default and only variant today.
The schedule object reports its upcoming fires in `next_run_times` (the next 5, empty while paused).
## The session template
`session_request` takes the same shape as the [Create session](/computer-use-agents/sessions/create) body. It is stored as a template and re-resolved on every fire, so a catalog agent like `"h/web-surfer-flash"` always runs its current version. Two restrictions apply: the template must contain at least one initial message, and it may not set `parent_session_id`. If the template does not set `max_time_s`, scheduled sessions default to 3600 seconds.
## Fire outcomes
Every fire is recorded in the schedule's [run history](/computer-use-agents/schedules/runs), whether or not it created a session:
| Status | Meaning |
| ----------------- | ---------------------------------------------------------------------------- |
| `created` | A session was created; the run carries its `session_id`. |
| `skipped_overlap` | The session from a previous fire was still active, so this fire was skipped. |
| `skipped_quota` | Your organization was at quota, so this fire was skipped. |
| `error` | Session creation failed; the run carries the `error` detail. |
Fires do not queue behind each other: a skipped fire is skipped for good, and the schedule simply fires again at the next cadence point. Run history is retained for 90 days.
## Pausing and failures
[Pause](/computer-use-agents/schedules/pause) stops future fires without deleting the schedule, and [Resume](/computer-use-agents/schedules/resume) recomputes the next fire from now. After 5 consecutive `error` fires, the schedule is paused automatically with an explanatory `pause_note`. A successful fire or a resume resets the counter.
[Trigger](/computer-use-agents/schedules/trigger) fires a schedule once immediately, even while paused, without affecting the regular cadence. Use it to test a template before the first scheduled fire.
## Consuming the results
Nobody is polling a scheduled session, so pair schedules with a [webhook](/computer-use-agents/webhooks/overview): you receive a signed event when each scheduled session reaches a settled state, then fetch its answer with [Get session](/computer-use-agents/sessions/retrieve). For batch inspection, list a schedule's sessions with the `schedule_id` filter on [List sessions](/computer-use-agents/sessions/list); each fire's [run record](/computer-use-agents/schedules/runs) also links to its session.
## Constraints
* An organization can have up to 20 schedules.
* Deleting a schedule stops future fires; sessions already created keep running.
# Pause a schedule
Source: https://hub.hcompany.ai/computer-use-agents/schedules/pause
POST /api/v2/schedules/{schedule_id}/pause
Stop future fires without deleting the schedule.
Pauses the schedule: automatic fires stop and `next_run_times` empties. The schedule can still be fired manually with [Trigger](/computer-use-agents/schedules/trigger).
**Returns** the updated [schedule object](/computer-use-agents/schedules/retrieve) with `paused: true`.
***
## Path parameters
The schedule's id (UUID).
## Request body
Optional note explaining why the schedule is paused (max 255 characters). Returned as `pause_note`.
***
## Examples
```bash cURL theme={null}
curl -X POST "https://agp.eu.hcompany.ai/api/v2/schedules/9f8e7d6c-5b4a-4c3d-8e2f-1a0b9c8d7e6f/pause" \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"note": "Paused during site maintenance"}'
```
```python Python theme={null}
from hai_agents import Client, PauseSchedule
client = Client()
schedule = client.schedules.pause_schedule(
"9f8e7d6c-5b4a-4c3d-8e2f-1a0b9c8d7e6f",
request=PauseSchedule(note="Paused during site maintenance"),
)
print(schedule.paused, schedule.pause_note)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const schedule = await client.schedules.pauseSchedule({
scheduleId: "9f8e7d6c-5b4a-4c3d-8e2f-1a0b9c8d7e6f",
body: { note: "Paused during site maintenance" },
});
console.log(schedule.paused, schedule.pauseNote);
```
***
## Errors
| Status | Cause |
| ------ | ---------------------------------------------- |
| `404` | No schedule with this id in your organization. |
# Resume a schedule
Source: https://hub.hcompany.ai/computer-use-agents/schedules/resume
POST /api/v2/schedules/{schedule_id}/resume
Restart a paused schedule.
Resumes a paused schedule: the next fire is recomputed from now (fires missed while paused are not made up), `pause_note` clears, and the consecutive-failure counter resets. Resuming a schedule that is not paused is a no-op.
**Returns** the updated [schedule object](/computer-use-agents/schedules/retrieve) with `paused: false`.
***
## Path parameters
The schedule's id (UUID).
***
## Examples
```bash cURL theme={null}
curl -X POST "https://agp.eu.hcompany.ai/api/v2/schedules/9f8e7d6c-5b4a-4c3d-8e2f-1a0b9c8d7e6f/resume" \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
schedule = client.schedules.resume_schedule("9f8e7d6c-5b4a-4c3d-8e2f-1a0b9c8d7e6f")
print(schedule.next_run_times[0])
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const schedule = await client.schedules.resumeSchedule({ scheduleId: "9f8e7d6c-5b4a-4c3d-8e2f-1a0b9c8d7e6f" });
console.log(schedule.nextRunTimes[0]);
```
***
## Errors
| Status | Cause |
| ------ | ---------------------------------------------- |
| `404` | No schedule with this id in your organization. |
# Retrieve a schedule
Source: https://hub.hcompany.ai/computer-use-agents/schedules/retrieve
GET /api/v2/schedules/{schedule_id}
Fetch a schedule by id.
**Returns** the schedule object.
***
## Path parameters
The schedule's id (UUID).
***
## The schedule object
Unique id (UUID).
Display name.
Optional description.
When the schedule fires: `type` (`"cron"`), `expression` (five-field cron), and `timezone` (IANA name the expression is evaluated in).
Template used to create each scheduled session, in the [Create session](/computer-use-agents/sessions/create) body shape. Re-resolved on every fire.
Whether the schedule is paused. Paused schedules do not fire automatically but can still be [triggered](/computer-use-agents/schedules/trigger).
Why the schedule is paused: the note passed to [Pause](/computer-use-agents/schedules/pause), or an explanatory note when it was paused automatically after repeated failures.
The next 5 fire times (UTC, RFC 3339). Empty while paused.
When the schedule last fired automatically. Manual triggers do not update it.
When the schedule was created.
When the schedule was last modified.
***
## Examples
```bash cURL theme={null}
curl "https://agp.eu.hcompany.ai/api/v2/schedules/9f8e7d6c-5b4a-4c3d-8e2f-1a0b9c8d7e6f" \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
schedule = client.schedules.get_schedule("9f8e7d6c-5b4a-4c3d-8e2f-1a0b9c8d7e6f")
print(schedule.name, schedule.next_run_times)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const schedule = await client.schedules.getSchedule({ scheduleId: "9f8e7d6c-5b4a-4c3d-8e2f-1a0b9c8d7e6f" });
console.log(schedule.name, schedule.nextRunTimes);
```
```json Response theme={null}
{
"id": "9f8e7d6c-5b4a-4c3d-8e2f-1a0b9c8d7e6f",
"name": "morning-market-scan",
"description": "Weekday scan of new Paris listings",
"timing": {"type": "cron", "expression": "0 9 * * 1-5", "timezone": "Europe/Paris"},
"session_request": {
"agent": "h/web-surfer-flash",
"messages": [
{"type": "user_message", "message": "Scan new apartment listings in Paris 11e and summarize the top five"}
],
"max_time_s": 3600
},
"paused": false,
"pause_note": null,
"next_run_times": [
"2026-07-06T07:00:00Z",
"2026-07-07T07:00:00Z",
"2026-07-08T07:00:00Z",
"2026-07-09T07:00:00Z",
"2026-07-10T07:00:00Z"
],
"last_run_at": "2026-07-03T07:00:00Z",
"created_at": "2026-06-20T12:00:00Z",
"updated_at": "2026-07-03T07:00:00Z"
}
```
***
## Errors
| Status | Cause |
| ------ | ---------------------------------------------- |
| `404` | No schedule with this id in your organization. |
# List schedule runs
Source: https://hub.hcompany.ai/computer-use-agents/schedules/runs
GET /api/v2/schedules/{schedule_id}/runs
Browse a schedule's fire history.
Returns the schedule's recent fires, newest first. Every fire produces a run record, including skipped and failed ones, so the history is gap-free. Runs are retained for 90 days.
**Returns** a paginated list of run records.
***
## Path parameters
The schedule's id (UUID).
## Query parameters
Page number (1-based).
Items per page. Maximum: `1000`.
Sort order. Options: `scheduled_for`, `-scheduled_for`.
***
## The run record
Unique id for this run (UUID).
The schedule that fired.
Outcome of the fire: `created`, `skipped_overlap` (previous session still active), `skipped_quota` (organization at quota), or `error`.
The fire's due time (UTC, RFC 3339).
The created session's id when `status` is `created`, otherwise `null`.
Failure detail when `status` is `error`.
Whether the fire came from [Trigger](/computer-use-agents/schedules/trigger) rather than the cron cadence.
When the run record was written.
***
## Examples
```bash cURL theme={null}
curl "https://agp.eu.hcompany.ai/api/v2/schedules/9f8e7d6c-5b4a-4c3d-8e2f-1a0b9c8d7e6f/runs" \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
page = client.schedules.list_schedule_runs("9f8e7d6c-5b4a-4c3d-8e2f-1a0b9c8d7e6f")
for run in page.items:
print(run.scheduled_for, run.status, run.session_id)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const page = await client.schedules.listScheduleRuns({ scheduleId: "9f8e7d6c-5b4a-4c3d-8e2f-1a0b9c8d7e6f" });
for (const run of page.items) {
console.log(run.scheduledFor, run.status, run.sessionId);
}
```
```json Response theme={null}
{
"items": [
{
"id": "3c2b1a09-8d7e-4f6a-b5c4-d3e2f1a0b9c8",
"schedule_id": "9f8e7d6c-5b4a-4c3d-8e2f-1a0b9c8d7e6f",
"status": "created",
"scheduled_for": "2026-07-03T07:00:00Z",
"session_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"error": null,
"triggered_manually": false,
"created_at": "2026-07-03T07:00:02Z"
},
{
"id": "2b1a0987-7c6d-4e5f-a4b3-c2d1e0f9a8b7",
"schedule_id": "9f8e7d6c-5b4a-4c3d-8e2f-1a0b9c8d7e6f",
"status": "skipped_overlap",
"scheduled_for": "2026-07-02T07:00:00Z",
"session_id": null,
"error": null,
"triggered_manually": false,
"created_at": "2026-07-02T07:00:01Z"
}
],
"page": 1,
"total": 2
}
```
***
## Errors
| Status | Cause |
| ------ | ---------------------------------------------- |
| `404` | No schedule with this id in your organization. |
# Trigger a schedule
Source: https://hub.hcompany.ai/computer-use-agents/schedules/trigger
POST /api/v2/schedules/{schedule_id}/trigger
Fire a schedule once, immediately.
Fires the schedule once now and returns the outcome synchronously. Works while paused, and the regular cadence is unaffected: `next_run_times` and `last_run_at` do not change. The fire goes through the same checks as an automatic one, so it can come back `skipped_overlap` or `skipped_quota` rather than `created`.
**Returns** the [run record](/computer-use-agents/schedules/runs) for this fire, with `triggered_manually: true`.
***
## Path parameters
The schedule's id (UUID).
***
## Examples
```bash cURL theme={null}
curl -X POST "https://agp.eu.hcompany.ai/api/v2/schedules/9f8e7d6c-5b4a-4c3d-8e2f-1a0b9c8d7e6f/trigger" \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
run = client.schedules.trigger_schedule("9f8e7d6c-5b4a-4c3d-8e2f-1a0b9c8d7e6f")
print(run.status, run.session_id)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const run = await client.schedules.triggerSchedule({ scheduleId: "9f8e7d6c-5b4a-4c3d-8e2f-1a0b9c8d7e6f" });
console.log(run.status, run.sessionId);
```
```json Response theme={null}
{
"id": "3c2b1a09-8d7e-4f6a-b5c4-d3e2f1a0b9c8",
"schedule_id": "9f8e7d6c-5b4a-4c3d-8e2f-1a0b9c8d7e6f",
"status": "created",
"scheduled_for": "2026-07-04T11:26:03Z",
"session_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"error": null,
"triggered_manually": true,
"created_at": "2026-07-04T11:26:05Z"
}
```
***
## Errors
| Status | Cause |
| ------ | ---------------------------------------------------------------------------------------------------- |
| `404` | No schedule with this id in your organization. |
| `409` | A fire for this schedule is already in progress; retry shortly (a `Retry-After` header is included). |
# Update a schedule
Source: https://hub.hcompany.ai/computer-use-agents/schedules/update
PATCH /api/v2/schedules/{schedule_id}
Change a schedule's name, timing, or session template.
Partial update: only the fields you provide change. Changing `timing` recomputes the next fire from now.
**Returns** the updated [schedule object](/computer-use-agents/schedules/retrieve).
***
## Path parameters
The schedule's id (UUID).
## Request body
New display name. May not be `null`.
New description. Pass `null` to clear it.
New cron timing (same shape and constraints as [Create](/computer-use-agents/schedules/create)). May not be `null`. The next fire is recomputed from now.
New session template (same shape and restrictions as [Create](/computer-use-agents/schedules/create)). May not be `null`.
***
## Examples
```bash cURL theme={null}
curl -X PATCH "https://agp.eu.hcompany.ai/api/v2/schedules/9f8e7d6c-5b4a-4c3d-8e2f-1a0b9c8d7e6f" \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"timing": {"expression": "30 8 * * 1-5", "timezone": "Europe/Paris"}}'
```
```python Python theme={null}
from hai_agents import Client
client = Client()
schedule = client.schedules.update_schedule(
"9f8e7d6c-5b4a-4c3d-8e2f-1a0b9c8d7e6f",
timing={"expression": "30 8 * * 1-5", "timezone": "Europe/Paris"},
)
print(schedule.next_run_times[0])
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const schedule = await client.schedules.updateSchedule({
scheduleId: "9f8e7d6c-5b4a-4c3d-8e2f-1a0b9c8d7e6f",
timing: { expression: "30 8 * * 1-5", timezone: "Europe/Paris" },
});
console.log(schedule.nextRunTimes[0]);
```
***
## Errors
| Status | Cause |
| ------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `400` | The new expression fires more often than once every 5 minutes, or the new template is invalid (no messages, or `parent_session_id` set). |
| `404` | No schedule with this id in your organization. |
| `422` | Body failed validation: invalid cron expression, unknown timezone, or explicit `null` for `name`, `timing`, or `session_request`. |
# Build with the SDKs
Source: https://hub.hcompany.ai/computer-use-agents/sdks
Typed Python and TypeScript clients, plus the CLI.
Call the API directly over HTTP, use a typed client, or connect any [MCP host](/computer-use-agents/mcp). The clients and CLI ship as `hai-agents`.
Sync and async clients, typed with Pydantic v2.
A fully typed client for sessions, agents, skills, environments, schedules, and webhooks.
## Install
```bash CLI theme={null}
pip install "hai-agents[cli]"
```
```bash Python theme={null}
pip install hai-agents
```
```bash TypeScript theme={null}
npm install hai-agents
```
## Authenticate
Set `HAI_API_KEY` in your environment, or pass the key explicitly; either way the client attaches it to every call as a bearer token. If you don't have a key yet, [create one](/computer-use-agents/quickstart#get-your-api-key) first.
```bash CLI theme={null}
hai login # browser sign-in; stores the key in ~/.config/hai/.env
```
```python Python theme={null}
from hai_agents import Client
client = Client()
# or pass it explicitly: Client(api_key="hk-...")
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
// or pass it explicitly: new HaiAgentsClient({ apiKey: "hk-..." })
```
The [API reference](/computer-use-agents/sessions/create) playground runs in your browser and shows raw HTTP, handy for exploring the wire format. Use the SDK snippets here and in the [Quickstart](/computer-use-agents/quickstart) for code you'd ship.
## Region
H runs isolated EU and US regions. Requests stay in-region, so an EU key only ever reaches EU infrastructure (data residency). The REST API lives under `/api/v2` and the [MCP server](/computer-use-agents/mcp) under `/mcp` on each region's host:
| Region | Host |
| ------------ | ---------------------------- |
| EU (default) | `https://agp.eu.hcompany.ai` |
| US | `https://agp.hcompany.ai` |
The clients default to the EU host. To target another region, pass it explicitly:
```python Python theme={null}
from hai_agents import Client, HaiAgentsEnvironment
client = Client(environment=HaiAgentsEnvironment.US)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient, HaiAgentsEnvironment } from "hai-agents";
const client = new HaiAgentsClient({ environment: HaiAgentsEnvironment.Us });
```
## Examples
The [`hcompai/computer-use-agents-demos`](https://github.com/hcompai/computer-use-agents-demos) repo collects recipes for the `hai-agents` SDK. Each one runs on its own and is wired up as an MCP server, a CLI tool, or both, so you can call the agents from Claude Code, Cursor, Codex, or Hermes.
| Example | What it shows | Interface |
| ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------- |
| [`qa/mcp`](https://github.com/hcompai/computer-use-agents-demos/tree/main/examples/qa/mcp) | An autonomous browser agent QAs a remote URL and returns a structured `{verdict, summary, findings}` | MCP server (`review_web_ui`, `visual_check`) |
| [`qa/cli`](https://github.com/hcompai/computer-use-agents-demos/tree/main/examples/qa/cli) | The same QA agent exposed as a shell command, surfaced to Claude Code via the `hai-qa-via-cli` skill | CLI (`qa-cli review / visual`) |
| [`extract_anything`](https://github.com/hcompai/computer-use-agents-demos/tree/main/examples/extract_anything) | Wrap an agent call as a typed function: a generic `extract(url, task, schema)` or curated `get_*` tools | MCP server (`extract`) + CLI (`extract-cli`) |
| [`counterfeit_detection`](https://github.com/hcompai/computer-use-agents-demos/tree/main/examples/counterfeit_detection) | A custom-tools cookbook in three stages: bare `run_session`, then local screenshot-compare tools, then a `max_steps` / `max_time_s` budget for an exhaustive sweep | CLI (`counterfeit-cli simple / tooled / sweep`) |
### See it in action
QA a live page from Claude Code: *"Use `review_web_ui` to check the top story link works and the page has reasonable accessibility."*
The video at the top of this page drives the agent straight from a natural-language prompt in Python: *"Search for 'Random Access Memories' by Daft Punk, add it to the shopping cart."* Runnable code: [`examples/add_to_cart/add_to_cart.py`](https://github.com/hcompai/computer-use-agents-demos/blob/main/examples/add_to_cart/add_to_cart.py)
## Next steps
Run and manage agents from Cursor, Claude Code, Codex, Hermes, and more, with no SDK code.
Install the `hai-agents` skill so Claude Code, Cursor, and other assistants know the H APIs and scaffold use cases for you.
Run your first session end to end in under 5 minutes.
Reusable configurations: pre-built agents and how to create your own.
# Cancel a session
Source: https://hub.hcompany.ai/computer-use-agents/sessions/cancel
DELETE /api/v2/sessions/{id}
Stop a running session.
Cancels a session that is still active. The agent is stopped and the session transitions to `interrupted` status. Cancelling a [`queued`](/computer-use-agents/observe-and-steer#queued-sessions) session dequeues it immediately, without it ever starting, and cancelling a session that already finished is a harmless no-op. Cancellation can't be undone: use [pause](/computer-use-agents/sessions/pause) if you want to resume later.
**Returns** `204 No Content` on success.
***
## Path parameters
The session ID to cancel.
***
## Examples
```bash CLI theme={null}
hai sessions cancel "$SESSION_ID" --yes
```
```bash cURL theme={null}
curl -X DELETE https://agp.eu.hcompany.ai/api/v2/sessions/$SESSION_ID \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
client.sessions.cancel_session(session_id)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
await client.sessions.cancelSession({ id: sessionId });
```
***
## When to cancel vs. pause
| Action | Effect | Can resume? | Frees quota? |
| ------------------------- | -------------------------------- | ----------- | ------------ |
| **Cancel** (`DELETE`) | Stops the agent, terminal state | No | Yes |
| **Pause** (`POST /pause`) | Halts the agent, preserves state | Yes | No |
Use cancel when:
* The task is no longer needed
* You want to free a concurrency slot immediately
* The agent is stuck or producing unhelpful output
Use [pause](/computer-use-agents/sessions/pause) when:
* You want to review progress before continuing
* You need to provide input later but not right now
***
## Errors
| Status | Cause |
| ------ | -------------------------------------------- |
| `404` | Session not found, or you don't have access. |
# Get session changes
Source: https://hub.hcompany.ai/computer-use-agents/sessions/changes
GET /api/v2/sessions/{id}/changes
Long-poll for real-time session updates.
Returns a stream of changes (events, status transitions, agent actions) that have occurred since your last request. Uses **long polling**: the server holds the connection open until new changes are available or the timeout expires.
**Returns** `200` with a `SessionChanges` object, or `204 No Content` if no new events arrive within the wait period.
`changes` is a delta: each call returns only what's new since `from_index`, and `204` when nothing new has arrived yet. Note that `status` can read `completed` while events (including the [`answer`](#response)) are still unread on later pages, so keep advancing `from_index` until the session is [terminal](/computer-use-agents/sessions/overview#lifecycle) *and* a poll returns no further events. To skip the loop entirely and just read a finished run's result, use [`latest_answer`](/computer-use-agents/sessions/retrieve); the [SDK helper](#long-polling-pattern) drains for you.
***
## Path parameters
The session ID.
***
## Query parameters
Event index to start from. Use this to resume from where you left off.
Maximum number of events to return.
Whether to include event details in the response.
How long the server should hold the connection waiting for changes, up to `25` seconds. The default `0` returns immediately; set `20` to `25` for efficient long polling.
***
## Response
```json Response theme={null}
{
"status": "running",
"started_at": "2026-05-07T14:30:02Z",
"finished_at": null,
"error": null,
"error_code": null,
"answer": null,
"outcome": null,
"metrics": {
"steps": 2,
"total_cost": 0.0093874,
"input_cost": 0.0084664,
"output_cost": 0.000921,
"reasoning_cost": 0.0,
"cost_per_model": [
{
"name": "holo3-122b-a10b",
"input_tokens": 21166,
"output_tokens": 307,
"reasoning_tokens": 0,
"input_cost": 0.0084664,
"output_cost": 0.000921,
"reasoning_cost": 0.0,
"total_cost": 0.0093874
}
]
},
"new_events": [
{ "type": "AgentEvent", "data": { "...": "..." }, "timestamp": "2026-05-07T14:30:05Z" }
]
}
```
| Field | Type | Description |
| ------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `status` | string | Current session status. |
| `started_at` | string \| null | ISO 8601 timestamp when the agent started executing. |
| `finished_at` | string \| null | ISO 8601 timestamp when the session reached a terminal state. |
| `error` | string \| null | Short, stable error message if the session failed or timed out. Branch on `error_code`, not on this text. |
| `error_code` | string \| null | Machine-readable failure category when the session failed or timed out: `environment_error`, `no_answer`, `answer_validation`, `timeout`, or `internal`. See [Read how the run ended](/computer-use-agents/observe-and-steer#read-how-the-run-ended). |
| `outcome` | string \| null | The agent's self-assessed task outcome, reported with its final answer: `success`, `partial`, `infeasible`, or `blocked`. See [Read how the run ended](/computer-use-agents/observe-and-steer#outcomes). |
| `answer` | string \| object \| null | The agent's final result once produced; `null` otherwise. A string by default, or an object matching the agent's [`answer_format`](/computer-use-agents/agents/overview) JSON Schema when one was set. It rides the page that delivers the final events, so keep polling until the session is [terminal](/computer-use-agents/sessions/overview#lifecycle) and drained. For a cursor-independent read, the same value is mirrored on the [Session object](/computer-use-agents/sessions/overview)'s `latest_answer`. |
| `metrics` | object | Usage and cost rolled up to the moment of the response: `steps`, `total_cost`, `input_cost`, `output_cost`, `reasoning_cost`, and `cost_per_model[]` (each entry carries per-model tokens and costs, including `reasoning_tokens`). Cost fields are in USD and are `null` when a model's price is unavailable, so null-check before summing. |
| `new_events` | array | Events since `from_index`, each following the [event shape](/computer-use-agents/sessions/events#event-shape) (`type`, `data`, `timestamp`). |
***
## Examples
```bash cURL theme={null}
curl "https://agp.eu.hcompany.ai/api/v2/sessions/$SESSION_ID/changes?from_index=0&wait_for_seconds=25" \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
changes = client.sessions.get_session_changes(
session_id,
from_index=0,
wait_for_seconds=25,
)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const changes = await client.sessions.getSessionChanges({
id: sessionId,
fromIndex: 0,
waitForSeconds: 25,
});
```
***
## Long-polling pattern
Long polling is more efficient than repeated status checks because the server only responds when something actually changes. The SDK ships a helper that runs the loop for you: it drives termination off `status` (authoritative) while streaming events from `changes`, resuming `from_index` and handling the `204` no-change responses automatically.
```python Python theme={null}
from hai_agents import wait_for_session
result = wait_for_session(client, session_id, wait_for_seconds=25)
for event in result.events:
print(event.type)
print(result.status, result.answer)
# client.run_session(...) creates the session and runs this loop in one call.
```
```typescript TypeScript theme={null}
import { waitForSession } from "hai-agents";
const result = await waitForSession(client, { id: sessionId, waitForSeconds: 25 });
for (const event of result.events) {
console.log(event.type);
}
console.log(result.status, result.answer);
// client.runSession(...) creates the session and runs this loop in one call.
```
Long-poll `changes` to follow a run: it returns new events and the final `answer` with near-instant latency and far fewer calls than fixed-interval polling. Reach for [`status`](/computer-use-agents/sessions/status) only when you want a cheap, one-off liveness check.
# Create a session
Source: https://hub.hcompany.ai/computer-use-agents/sessions/create
POST /api/v2/sessions
Launch a new agent run.
Creates a new session that runs an agent against the given task. When a slot is available the session starts in `pending` status and transitions to `running` once the agent launches. A create above your [concurrency limit](/computer-use-agents/plans-and-limits#concurrent-sessions) is accepted as `queued` and starts automatically when a slot frees up (set [`queue: false`](#body-queue) to get a `429` instead).
**Returns** the created [Session](/computer-use-agents/sessions/overview) object with status `pending` or `queued`.
***
## Headers
Optional idempotency key (max 255 characters). Safe retry within 24 hours: reusing the same key with a different body returns `422`.
***
## Request body
Either a catalog identifier (string, e.g. `"h/web-surfer-flash"`) or an inline [Agent](/computer-use-agents/agents/overview) object. An inline agent must include its own `environments` (at most one per kind), unless it is a pure [manager](/computer-use-agents/multi-agent) that only delegates to `subagents`; the session resolves only `agent` and reads everything else (environments, skills, subagents) from there.
Using a catalog id (environments come from the agent's stored spec):
```json theme={null}
"agent": "h/web-surfer-flash"
```
Inline agent, with environments nested under it:
```json theme={null}
"agent": {
"name": "weather-agent",
"description": "Looks up weather by city",
"environments": [
{
"id": "browser",
"kind": "web",
"mode": {"type": "visual", "width": 1280, "height": 720},
"start_url": "https://www.bing.com/"
}
]
}
```
See [Browser](/computer-use-agents/browser/configuration) for its config and fields.
Initial messages queued before the agent's first step. Usually a single user message describing the task; a plain string is accepted as shorthand for one user message.
Each message object has:
* `type` (string, optional): `"user_message"`, the default.
* `message` (string): The instruction or task description.
* `images` (array, optional): Base64 data URIs to attach (e.g. `data:image/png;base64,...`).
* `caller_id` (string, optional): Identifies the message sender. Defaults to `user`; leave it unset for normal user input.
```json theme={null}
"messages": [
{"type": "user_message", "message": "Book a flight from Paris to Tokyo on June 15"}
]
```
Cap on the number of steps the agent may take, where each step is one decide-and-act cycle. On reaching the cap the agent is asked to produce a final answer from what it has so far (it is **not** hard-killed), so you still get a structured result. Omit it to run uncapped.
Cap on wall-clock seconds. On reaching the cap, like `max_steps`, the agent is asked for a final answer rather than terminated abruptly. Omit it to run uncapped.
Switches between one-shot and interactive. Leave it `null` for a one-shot task: the session ends as soon as the agent answers. Set it (in seconds) to keep the session open for follow-up [messages](/computer-use-agents/sessions/send-messages): after each answer the session enters the [`idle`](/computer-use-agents/sessions/overview#lifecycle) status and waits this long for your next message before terminating.
Minutes after the session finishes before it is automatically deleted, along with its events and screenshots. Defaults to 30 days. Set `null` to keep the session forever.
Minutes after the session finishes before its screenshots are deleted, so you can expire visual data sooner than the session record. Defaults to 30 days. Set `null` to keep screenshots for the session's lifetime.
When you are at your [concurrency limit](/computer-use-agents/plans-and-limits#concurrent-sessions), accept this session into a queue (status [`queued`](/computer-use-agents/sessions/overview#lifecycle)) instead of rejecting it with `429`. Queued sessions don't count against your quota and start automatically, oldest first, as running sessions finish. Ideal for batch workloads: fire N tasks, then collect results via [webhooks](/computer-use-agents/webhooks/overview). Set `false` to fail fast with `429` when at capacity.
Tag for grouping related sessions. You can later query all sessions with `GET /sessions?group_id=...`.
ID of a parent session, for [multi-agent](/computer-use-agents/multi-agent) orchestration. The parent's status endpoint will include this session in its `subagent_session_ids` list. Child sessions never queue: at capacity the create fails with `429` even when `queue` is `true`, because queueing a child behind its own parent's slot would deadlock the parent.
Per-run tweaks applied after the agent (and its environments, skills, and subagents) are resolved, so you can adjust a catalog agent for a single run without editing its stored spec. Keys are dotted paths into the request; list members are addressed with an explicit `[field=value]` selector. Each value is validated against the field its path targets, so an unknown path or a wrong type is rejected with `422` at creation.
Common uses: point the browser at a different start URL, or ask a catalog agent for [structured output](/computer-use-agents/structured-output) by overriding its [`answer_format`](/computer-use-agents/agents/overview).
```json theme={null}
"overrides": {
"agent.environments[kind=web].start_url": "https://www.bing.com/",
"agent.answer_format": {
"type": "object",
"properties": {"price": {"type": "number"}},
"required": ["price"]
}
}
```
***
## Response
Unique session identifier.
The original session request body.
Session status object with `status: "pending"` for a newly created session, or `"queued"` when the create was accepted above your concurrency limit.
Link to the session's [Agent View](/computer-use-agents/observe-and-steer) page for live viewing and replay.
ISO 8601 timestamp.
***
## Examples
### Basic session
Reference a catalog agent; its stored spec supplies the environments:
```bash CLI theme={null}
# `hai run` creates the session and blocks until the agent answers
hai run "Find the best-rated sushi restaurants in San Francisco" \
--agent h/web-surfer-flash
```
```bash cURL theme={null}
curl -X POST https://agp.eu.hcompany.ai/api/v2/sessions \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"agent": "h/web-surfer-flash",
"messages": [
{"type": "user_message", "message": "Find the best-rated sushi restaurants in San Francisco"}
]
}'
```
```python Python theme={null}
from hai_agents import Client
client = Client()
session = client.sessions.create_session(
agent="h/web-surfer-flash",
messages="Find the best-rated sushi restaurants in San Francisco",
)
print(session.id)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const session = await client.sessions.createSession({
body: {
agent: "h/web-surfer-flash",
messages: "Find the best-rated sushi restaurants in San Francisco",
},
});
console.log(session.id);
```
```json Response theme={null}
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"request": {
"agent": "h/web-surfer-flash",
"messages": [
{"type": "user_message", "message": "Find the best-rated sushi restaurants in San Francisco"}
]
},
"status": {
"status": "pending",
"error": null,
"steps": 0,
"usage_per_model": [],
"subagent_session_ids": []
},
"agent_view_url": "https://platform.hcompany.ai/agents/sessions/a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"latest_answer": null,
"created_at": "2026-05-07T14:30:00Z",
"started_at": null,
"finished_at": null
}
```
### With idempotency key
```bash cURL theme={null}
curl -X POST https://agp.eu.hcompany.ai/api/v2/sessions \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: my-unique-key-123" \
-d '{
"agent": "h/web-surfer-flash",
"messages": [
{"type": "user_message", "message": "Search for direct flights from CDG to NRT on June 15"}
]
}'
```
```python Python theme={null}
session = client.sessions.create_session(
agent="h/web-surfer-flash",
messages="Search for direct flights from CDG to NRT on June 15",
idempotency_key="my-unique-key-123",
)
```
```typescript TypeScript theme={null}
const session = await client.sessions.createSession({
idempotencyKey: "my-unique-key-123",
body: {
agent: "h/web-surfer-flash",
messages: "Search for direct flights from CDG to NRT on June 15",
},
});
```
### With an inline agent and explicit browser environment
Pass an inline `Agent` instead of a catalog id when you want to override the environments (or any other field) on a per-session basis. Environments must be nested under `agent`.
```bash cURL theme={null}
curl -X POST https://agp.eu.hcompany.ai/api/v2/sessions \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"agent": {
"name": "web-price-finder",
"description": "Web browsing agent with a custom browser config",
"environments": [
{"id": "browser", "kind": "web", "mode": {"type": "visual", "width": 1280, "height": 720}, "start_url": "https://www.bing.com"}
]
},
"messages": [
{"type": "user_message", "message": "Find the current price of the Framework 13 laptop and report the lowest you find"}
]
}'
```
```python Python theme={null}
session = client.sessions.create_session(
agent={
"name": "web-price-finder",
"description": "Web browsing agent with a custom browser config",
"environments": [
{
"id": "browser",
"kind": "web",
"mode": {"type": "visual", "width": 1280, "height": 720},
"start_url": "https://www.bing.com",
}
],
},
messages="Find the current price of the Framework 13 laptop and report the lowest you find",
)
```
```typescript TypeScript theme={null}
const session = await client.sessions.createSession({
body: {
agent: {
name: "web-price-finder",
description: "Web browsing agent with a custom browser config",
environments: [
{
id: "browser",
kind: "web",
mode: { type: "visual", width: 1280, height: 720 },
startUrl: "https://www.bing.com",
},
],
},
messages: "Find the current price of the Framework 13 laptop and report the lowest you find",
},
});
```
### With per-run overrides
Reuse a catalog agent but tweak it for this run only: here we send it to a different start URL without editing its stored spec.
```bash CLI theme={null}
hai run "Find the cheapest direct flight CDG to NRT next month" \
--agent h/web-surfer-flash \
--override 'agent.environments[kind=web].start_url=https://www.google.com/travel/flights'
```
```bash cURL theme={null}
curl -X POST https://agp.eu.hcompany.ai/api/v2/sessions \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"agent": "h/web-surfer-flash",
"messages": [
{"type": "user_message", "message": "Find the cheapest direct flight CDG to NRT next month"}
],
"overrides": {
"agent.environments[kind=web].start_url": "https://www.google.com/travel/flights"
}
}'
```
```python Python theme={null}
session = client.sessions.create_session(
agent="h/web-surfer-flash",
messages="Find the cheapest direct flight CDG to NRT next month",
overrides={
"agent.environments[kind=web].start_url": "https://www.google.com/travel/flights",
},
)
```
```typescript TypeScript theme={null}
const session = await client.sessions.createSession({
body: {
agent: "h/web-surfer-flash",
messages: "Find the cheapest direct flight CDG to NRT next month",
overrides: {
"agent.environments[kind=web].start_url": "https://www.google.com/travel/flights",
},
},
});
```
### Child session (multi-agent)
See [Multi-agent](/computer-use-agents/multi-agent) for the full picture.
```bash cURL theme={null}
curl -X POST https://agp.eu.hcompany.ai/api/v2/sessions \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"agent": "h/web-surfer-flash",
"parent_session_id": "parent-session-uuid",
"group_id": "trip-planning-001",
"messages": [
{"type": "user_message", "message": "Find hotels near Shinjuku station under $150/night"}
]
}'
```
```python Python theme={null}
session = client.sessions.create_session(
agent="h/web-surfer-flash",
parent_session_id="parent-session-uuid",
group_id="trip-planning-001",
messages="Find hotels near Shinjuku station under $150/night",
)
```
```typescript TypeScript theme={null}
const session = await client.sessions.createSession({
body: {
agent: "h/web-surfer-flash",
parentSessionId: "parent-session-uuid",
groupId: "trip-planning-001",
messages: "Find hotels near Shinjuku station under $150/night",
},
});
```
***
## Errors
| Status | Cause |
| ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400` | The resolved request is invalid (for example an override producing an impossible configuration). |
| `402` | Your organization's monthly token budget is exhausted. See [Plans and limits](/computer-use-agents/plans-and-limits#token-usage). |
| `404` | The referenced agent doesn't exist or isn't visible to you. |
| `409` | An `Idempotency-Key` from a still in-flight request was reused before it completed. Retry after `Retry-After`. |
| `422` | Request body failed validation, or an `Idempotency-Key` was reused with a different body. |
| `429` | Concurrency quota exceeded with [`queue: false`](#body-queue), the queue itself is full, or the create carries a [`parent_session_id`](#body-parent-session-id) while at capacity. See [Plans and limits](/computer-use-agents/plans-and-limits#concurrent-sessions). |
# List events
Source: https://hub.hcompany.ai/computer-use-agents/sessions/events
GET /api/v2/sessions/{id}/events
Paginated list of session events.
Returns a paginated list of events for a session. Use [`/changes`](/computer-use-agents/sessions/changes) for live tailing; this endpoint is for historical pagination.
Auth is optional, so public shares are supported.
**Returns** a paginated list of `TrajectoryEvent` objects.
***
## Path parameters
The session ID.
***
## Query parameters
Page number (1-based).
Items per page. Maximum: `200`.
Sort order. Options: `timestamp`, `-timestamp`.
Filter by event type, e.g. `AgentEvent` or `MetricsUpdateEvent`. See [Event shape](#event-shape) for the full list.
***
## Event shape
Every event, on both this endpoint and [`/changes`](/computer-use-agents/sessions/changes), is the same envelope:
```json Event envelope theme={null}
{ "type": "AgentEvent", "data": { "...": "..." }, "timestamp": "2026-06-01T15:14:05Z" }
```
| Field | Type | Description |
| ----------- | ------ | ------------------------------------- |
| `type` | string | The event type (see below). |
| `data` | object | Type-specific payload. |
| `timestamp` | string | ISO 8601 time the event was recorded. |
### Event types (`type`)
The most common event types are below. `data` is an open JSON object whose shape varies by type, so treat this list as representative rather than exhaustive (other types such as `DelayAgentStartEvent`, `AgentRunStatusChangeEvent`, and `LiveViewUrlEvent` may appear).
| `type` | `data` | Emitted when |
| ----------------------------- | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `RequestStartEvent` | `{}` | The session request is received. |
| `RequestStartDispatchedEvent` | `{ "status": "..." }` | The run is dispatched for execution. |
| `AgentStartedEvent` | `{}` | The agent begins executing. |
| `AgentEvent` | the step event (see below) | The agent observes, decides, acts, or answers. |
| `MetricsUpdateEvent` | `{ "metrics": {...} }` | Usage and cost are rolled up. `metrics` carries `steps`, `total_cost`, `input_cost`, `output_cost`, `reasoning_cost`, and `cost_per_model[]`. Cost fields are in USD and `null` when a model's price is unavailable. |
| `ActiveStateChangeEvent` | `{ "state": "..." }` | The agent's active state changes (`running`, `idle`, or `awaiting_tool_results`; the latter also carries `pending_tool_calls` for [custom tools](/computer-use-agents/custom-tools)). |
| `AgentCompletionEvent` | `{ "reason": "..." }` | The run ends (e.g. `"finished"`). |
| `AgentErrorEvent` | `{ "error": "...", "trace": "...", "info": ... }` | The run fails; the session moves to `failed`. |
### Agent activity (`AgentEvent.data`)
The agent's step-by-step trace lives inside `AgentEvent`. Its `data` is the step event; `data.kind` tells you what happened:
| `data.kind` | Payload (under `data`) | Meaning |
| ------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `policy_event` | `reasoning_content`, `content`, `tool_reqs[]`, `validation_errors[]` | The agent's decision: its reasoning, message, and the action(s) it chose. |
| `observation_event` | `type`, `text`, `image`, `metadata` (see [Observation shapes](#observation-shapes)) | What the agent perceived this step. |
| `tool_result` | `tool_req` (`tool_name`, `args`, `id`), `result` | The outcome of an action; `result` is opaque JSON. |
| `answer_event` | `answer` | The final answer. Same value as [`/changes`](/computer-use-agents/sessions/changes) `answer`. |
| `error_event` | `error`, `origin`, `tool_req?` | A recoverable error during the step (e.g. a failed or invalid action). |
| `message_event` | `caller_id`, `content[]` | A user or agent message. |
| `flow_event` | `flow`, `origin` | A control-flow signal such as `pause`, `resume`, or `force_answer`, including the ones you trigger via the [session controls](/computer-use-agents/sessions/pause). |
Each `tool_reqs[]` entry (and `tool_result.tool_req`) is `{ tool_name, args, id }`. `policy_event.content` and `reasoning_content` may be `null`.
Parsing a stream means switching on the outer `type`, then for `AgentEvent`, on `data.kind`:
```json AgentEvent theme={null}
{
"type": "AgentEvent",
"timestamp": "2026-06-01T15:14:05Z",
"data": {
"kind": "observation_event",
"type": "web",
"text": null,
"image": { "type": "url", "source": "https://.../screenshot-7f3a.png", "media_type": "image/png" },
"metadata": {
"url": "https://example.com",
"title": "Example Domain",
"text": "# Example Domain\nThis domain is for use in illustrative examples...",
"tabs": ["0"],
"current_tab": "0",
"viewport_size": [1200, 1200],
"page_size": [1200, 3000],
"scroll_position": [0, 0]
}
}
}
```
### Observation shapes
An `observation_event` is flat: `type` names the environment that produced it, `image` is the screenshot (if any), and `metadata` is that environment's snapshot, shaped by `type` (an open object, so switch on `type` to read it). The Browser emits one of two `type`s, depending on the environment's [`mode`](/computer-use-agents/browser/configuration).
The page text lives in `metadata`, not in the top-level `text`: read `metadata.text` for `web` and `metadata.page_markdown` (or `page_html`) for `textual_web`. The top-level `text` carries only occasional step notices (e.g. `[page unchanged since previous observation]`) and is `null` for most observations.
**`web`** is emitted in `visual` mode: a screenshot (top-level `image`) plus page `metadata`.
| `metadata` field | Type | Description |
| ----------------- | ------ | ------------------------------------------------------------------------------- |
| `url` | string | Current page URL. |
| `title` | string | Page title. |
| `text` | string | Visible page rendered as markdown. Empty unless the mode sets `markdown: true`. |
| `tabs` | array | Open tab IDs. |
| `current_tab` | string | Active tab ID. |
| `viewport_size` | array | Viewport `[width, height]` in pixels. |
| `page_size` | array | Full page `[width, height]` in pixels. |
| `scroll_position` | array | Scroll offset `[x, y]` in pixels. |
| `cursor_position` | array | Cursor `[x, y]` in pixels, or `null`. |
**`textual_web`** is emitted in `text` mode: paginated page text under `metadata`, no screenshot.
| `metadata` field | Type | Description |
| ---------------- | ------- | -------------------------------------------- |
| `url` | string | Current page URL. |
| `title` | string | Page title. |
| `tabs` | array | Open tab IDs. |
| `current_tab` | string | Active tab ID. |
| `mode` | string | `"markdown"` or `"html"`. |
| `page_markdown` | string | Full page rendered as markdown. |
| `page_html` | string | Full page HTML. |
| `text_offset` | integer | Start character offset of the current chunk. |
| `chunk_size` | integer | Characters per chunk. |
| `chunk_number` | integer | Current chunk, 1-based. |
| `total_chunks` | integer | Total chunks for the page. |
### Images
The `image` on an `observation_event`, and any image inside `message_event.content[]`, is `{ source, type, media_type }`. `type` is `url` for platform-served screenshots (fetch `source` with your API key) or `base64` for inline images. Images embedded in a `tool_result.result` or an `answer_event.answer` stay inline as base64 within that opaque payload, never as URLs.
***
## Examples
```bash cURL theme={null}
curl "https://agp.eu.hcompany.ai/api/v2/sessions/$SESSION_ID/events?size=10" \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
page = client.sessions.list_session_events(session_id, size=10)
for event in page.items:
print(event.type, event.timestamp)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const page = await client.sessions.listSessionEvents({ id: sessionId, size: 10 });
for (const event of page.items) {
console.log(event.type, event.timestamp);
}
```
# Session feedback
Source: https://hub.hcompany.ai/computer-use-agents/sessions/feedback
POST /api/v2/sessions/{id}/feedback
Submit feedback on a session or individual event.
Submit feedback on a session you own. Use this to report whether the agent completed its task successfully.
**Returns** `204 No Content`.
***
## Path parameters
The session ID.
***
## Request body
Whether the session completed its task successfully.
Optional feedback message with details.
***
## Examples
### Session feedback
```bash cURL theme={null}
curl -X POST https://agp.eu.hcompany.ai/api/v2/sessions/$SESSION_ID/feedback \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"success": true,
"message": "Task completed correctly"
}'
```
```python Python theme={null}
from hai_agents import Client
client = Client()
client.sessions.submit_session_feedback(
session_id,
success=True,
message="Task completed correctly",
)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
await client.sessions.submitSessionFeedback({
id: sessionId,
body: {
success: true,
message: "Task completed correctly",
},
});
```
### Event feedback
You can also submit feedback on a specific event within the session:
```
PUT /api/v2/sessions/{id}/events/{event_index}/feedback
```
Same body, with `event_index` identifying the event by its position in the session's [event list](/computer-use-agents/sessions/events). **Returns** `204 No Content`, or `404` when the index is out of range.
```bash cURL theme={null}
curl -X PUT https://agp.eu.hcompany.ai/api/v2/sessions/$SESSION_ID/events/5/feedback \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"success": false,
"message": "Agent clicked the wrong button at this step"
}'
```
```python Python theme={null}
from hai_agents import Client
client = Client()
client.sessions.submit_event_feedback(
session_id,
5,
success=False,
message="Agent clicked the wrong button at this step",
)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
await client.sessions.submitEventFeedback({
id: sessionId,
eventIndex: 5,
body: {
success: false,
message: "Agent clicked the wrong button at this step",
},
});
```
# Force an answer
Source: https://hub.hcompany.ai/computer-use-agents/sessions/force-answer
POST /api/v2/sessions/{id}/force_answer
Ask the agent to emit a final answer on its next step.
Injects a `force_answer` flow-control event into the session. The agent stops exploring and produces a final answer on its next step, based on whatever context it has gathered so far. Like [pause](/computer-use-agents/sessions/pause) and [resume](/computer-use-agents/sessions/resume), this request takes no body, requires authentication, and does not wait for the agent to act.
If the session has [subagents](/computer-use-agents/multi-agent) still running, `force_answer` propagates down to them: each in-flight subagent is asked to wrap up, gets a short grace window to finalize, and any that don't finish in time are cancelled. Their partial results are folded into the parent's answer. The same cascade repeats through any deeper subagents.
On a [`queued`](/computer-use-agents/observe-and-steer#queued-sessions) session the event is buffered and delivered when the agent starts. On a finished session it behaves like [sending a message](/computer-use-agents/sessions/send-messages): the session restarts and the relaunched agent answers immediately.
**Returns** `202 Accepted`. The command is delivered asynchronously.
***
## Path parameters
The session ID.
***
## Examples
```bash cURL theme={null}
curl -X POST https://agp.eu.hcompany.ai/api/v2/sessions/$SESSION_ID/force_answer \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
client.sessions.force_session_answer(session_id)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
await client.sessions.forceSessionAnswer({ id: sessionId });
```
***
## Errors
| Status | Cause |
| ------ | -------------------------------------------- |
| `404` | Session not found, or you don't have access. |
# List sessions
Source: https://hub.hcompany.ai/computer-use-agents/sessions/list
GET /api/v2/sessions
Browse your sessions with filtering, sorting, and pagination.
Returns a paginated list of sessions visible to the authenticated user. Results are sorted by creation date (newest first) by default.
**Returns** a paginated list of [Session](/computer-use-agents/sessions/overview) summary objects. Each row carries `last_activity_at` — when the session's latest event was ingested (agent steps, status changes, user messages) — so you can spot which live sessions are actually moving. It is `null` for sessions that predate the field.
***
## Query parameters
Page number (1-based).
Items per page. Maximum: `100`.
Sort order. Options: `created_at`, `-created_at`.
Filter by session status. Multi-value. Values: `queued`, `pending`, `running`, `paused`, `idle`, `awaiting_tool_results`, `completed`, `failed`, `timed_out`, `interrupted`.
Filter by agent identifier. Multi-value. Example: `web-price-finder`.
Filter by group ID. Returns all sessions tagged with this group.
Filter by parent session ID. Returns only child sessions of the given parent.
Filter by [schedule](/computer-use-agents/schedules/overview) ID. Returns only sessions created by that schedule's fires.
Case-insensitive match on the session's first message or answer.
Only sessions created before this timestamp (ISO 8601).
Only sessions created after this timestamp (ISO 8601).
Only sessions that finished before this timestamp (ISO 8601).
Only sessions that finished after this timestamp (ISO 8601).
Access scope: `me` (your sessions anywhere), `me-in-organization` (your sessions in the current org), `organization` (everyone's sessions in the org), or `me-or-organization`.
***
## Examples
### List your recent sessions
```bash CLI theme={null}
hai sessions list --size 5
```
```bash cURL theme={null}
curl "https://agp.eu.hcompany.ai/api/v2/sessions?size=5" \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
page = client.sessions.list_sessions(size=5)
for summary in page.items:
print(summary.id, summary.status)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const page = await client.sessions.listSessions({ size: 5 });
for (const summary of page.items) {
console.log(summary.id, summary.status);
}
```
```json Response theme={null}
{
"items": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"agent": "h/web-surfer-flash",
"status": "completed",
"agent_view_url": "https://platform.hcompany.ai/agents/sessions/a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"first_message": {"type": "user_message", "message": "Top 3 stories on Hacker News?", "images": [], "caller_id": "user"},
"created_at": "2026-05-07T14:30:00Z",
"started_at": "2026-05-07T14:30:02Z",
"finished_at": "2026-05-07T14:31:15Z",
"last_activity_at": "2026-05-07T14:31:15Z"
},
{
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"agent": "web-price-finder",
"status": "running",
"agent_view_url": "https://platform.hcompany.ai/agents/sessions/b2c3d4e5-f6a7-8901-bcde-f12345678901",
"first_message": {"type": "user_message", "message": "Find direct flights CDG to NRT", "images": [], "caller_id": "user"},
"created_at": "2026-05-07T14:25:00Z",
"started_at": "2026-05-07T14:25:01Z",
"finished_at": null,
"last_activity_at": "2026-05-07T14:29:58Z"
}
],
"page": 1,
"total": 47
}
```
### Filter by status and agent
```bash cURL theme={null}
curl "https://agp.eu.hcompany.ai/api/v2/sessions?status=running&agent=web-price-finder" \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
page = client.sessions.list_sessions(status=["running"], agent=["web-price-finder"])
```
```typescript TypeScript theme={null}
const page = await client.sessions.listSessions({
status: ["running"],
agent: ["web-price-finder"],
});
```
### List all sessions in a group
```bash cURL theme={null}
curl "https://agp.eu.hcompany.ai/api/v2/sessions?group_id=trip-planning-001" \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
page = client.sessions.list_sessions(group_id="trip-planning-001")
```
```typescript TypeScript theme={null}
const page = await client.sessions.listSessions({ groupId: "trip-planning-001" });
```
### Find the subagents a session spawned
[Multi-agent](/computer-use-agents/multi-agent) runs delegate work to child sessions. The parent's [status](/computer-use-agents/sessions/status) lists their IDs in `subagent_session_ids`; pass the parent's ID to `parent_session_id` to pull the whole roster in one call, each child labeled with the `agent` that ran it. Walk deeper trees by recursing on a child's own ID.
```bash cURL theme={null}
curl "https://agp.eu.hcompany.ai/api/v2/sessions?parent_session_id=$PARENT_SESSION_ID" \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
page = client.sessions.list_sessions(parent_session_id=parent_session_id)
```
```typescript TypeScript theme={null}
const page = await client.sessions.listSessions({ parentSessionId });
```
```json Response theme={null}
{
"items": [
{
"id": "c3d4e5f6-a7b8-9012-cdef-234567890abc",
"agent": "fast-searcher",
"status": "completed",
"agent_view_url": "https://platform.hcompany.ai/agents/sessions/c3d4e5f6-a7b8-9012-cdef-234567890abc",
"first_message": {"type": "user_message", "message": "Search EV market-share figures for 2025", "images": [], "caller_id": "user"},
"created_at": "2026-05-07T14:30:05Z",
"started_at": "2026-05-07T14:30:06Z",
"finished_at": "2026-05-07T14:30:41Z",
"last_activity_at": "2026-05-07T14:30:41Z"
},
{
"id": "d4e5f6a7-b8c9-0123-def4-34567890abcd",
"agent": "visual-verifier",
"status": "running",
"agent_view_url": "https://platform.hcompany.ai/agents/sessions/d4e5f6a7-b8c9-0123-def4-34567890abcd",
"first_message": {"type": "user_message", "message": "Verify the 2025 figure on the source page", "images": [], "caller_id": "user"},
"created_at": "2026-05-07T14:30:42Z",
"started_at": "2026-05-07T14:30:43Z",
"finished_at": null,
"last_activity_at": "2026-05-07T14:30:55Z"
}
],
"page": 1,
"total": 2
}
```
Each child is a session like any other: open it by `id` to poll its [status](/computer-use-agents/sessions/status), read its answer from [`/changes`](/computer-use-agents/sessions/changes), or replay its [events](/computer-use-agents/sessions/events).
# Sessions
Source: https://hub.hcompany.ai/computer-use-agents/sessions/overview
A session represents a single execution of an agent.
A session moves through a fixed [lifecycle](#lifecycle). You can steer it while it runs and read the result when it finishes. Every follow-up call (sending a message, pausing, cancelling) is addressed to the session's `id`. The optional [`max_steps` and `max_time_s`](/computer-use-agents/sessions/create) caps bound how long it runs before the agent is asked for a final answer.
```bash CLI theme={null}
hai run "Top 3 stories on Hacker News?" \
--agent h/web-surfer-flash
```
```bash cURL theme={null}
SESSION=$(curl -s -X POST https://agp.eu.hcompany.ai/api/v2/sessions \
-H "Authorization: Bearer $HAI_API_KEY" -H "Content-Type: application/json" \
-d '{"agent": "h/web-surfer-flash", "messages": [{"type": "user_message", "message": "Top 3 stories on Hacker News?"}]}' | jq -r .id)
echo "$SESSION"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
session = client.sessions.create_session(
agent="h/web-surfer-flash",
messages=[{"type": "user_message", "message": "Top 3 stories on Hacker News?"}],
)
print(session.id)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const session = await client.sessions.createSession({
body: {
agent: "h/web-surfer-flash",
messages: [{ type: "user_message", message: "Top 3 stories on Hacker News?" }],
},
});
console.log(session.id);
```
Creating a session returns its `id`, which you poll for progress and the answer as in the [Quickstart](/computer-use-agents/quickstart). For a one-shot run, the helper below blocks until the agent finishes and hands back the result.
```python Python theme={null}
result = client.run_session(
agent="h/web-surfer-flash",
messages="Top 3 stories on Hacker News?",
)
print(result.status, result.answer)
```
```typescript TypeScript theme={null}
const result = await client.runSession({
agent: "h/web-surfer-flash",
messages: "Top 3 stories on Hacker News?",
});
console.log(result.status, result.answer);
```
Pick the call that matches how much control you need:
| You want to… | SDK call | You get back |
| --------------------------------------------------- | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| Run and read the answer in one shot | `client.run_session(...)` / `runSession` | Blocks, then returns the final result |
| Watch or steer while it runs | `client.start_session(...)` / `startSession` | A handle bound to the `id`; read and steer, then `wait_for_completion` |
| Drive the loop yourself (raw HTTP, other languages) | `POST /sessions`, then long-poll [`changes`](/computer-use-agents/sessions/changes) | The session `id`; you poll |
### Reading the answer
The simplest is the session snapshot. [`GET /sessions/{id}`](/computer-use-agents/sessions/retrieve) returns `latest_answer`, the agent's most recent final answer, or `null` until it first answers. It needs no cursor and never goes stale, so once a run has settled it is the easiest way to read the answer.
While a run is active, [`GET /sessions/{id}/changes`](/computer-use-agents/sessions/changes) carries the same `answer` alongside the live event feed. Because `changes` returns only what is new since your cursor (and `204 No Content` when nothing new has arrived), the answer rides the page that delivers the final events. Keep polling until the session reaches a [terminal state](#lifecycle) and you have drained the remaining events; a `204` means no new events yet, not no answer. The SDK helpers ([`run_session` / `wait_for_session`](/computer-use-agents/sessions/changes#long-polling-pattern)) run this loop and drain to the end for you.
Don't poll [`status`](/computer-use-agents/sessions/status) for the answer: it never carries one.
## Session object
| Field | Description |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `id` | The session's UUID, the handle for every follow-up call. |
| `request` | Echoes what you submitted, with `agent` resolved to its full spec even if you passed a catalog id. |
| `status` | Carries the live `status`, step count, per-model token usage (`usage_per_model`), any `error` and its `error_code`, the agent's self-assessed `outcome`, and `subagent_session_ids`. See [Session status](/computer-use-agents/sessions/status) for the breakdown. |
| `agent_view_url` | Link to the session's [Agent View](/computer-use-agents/observe-and-steer) page for live viewing and replay. |
| `latest_answer` | The agent's most recent final answer, mirrored from [`changes`](/computer-use-agents/sessions/changes); `null` until it first answers. |
| `created_at` / `started_at` / `finished_at` | Track the run's timeline; the latter two are `null` until they happen. |
## Lifecycle
Every session moves through the same state machine, whichever agent runs it:
| Status | Meaning | Terminal |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| `queued` | Session accepted above your [concurrency limit](/computer-use-agents/plans-and-limits#concurrent-sessions); it starts automatically, oldest first, as slots free up. | No |
| `pending` | Session created, agent is launching. | No |
| `running` | Agent is actively working on the task. | No |
| `paused` | Manually paused via the API. State is preserved. | No |
| `idle` | Interactive agent finished a task and is waiting for your next message. | No |
| `awaiting_tool_results` | Agent is blocked on [custom tool](/computer-use-agents/custom-tools) calls your code must answer. | No |
| `completed` | Agent finished the task successfully. | Yes |
| `timed_out` | Agent exceeded the maximum allowed time. | Yes |
| `interrupted` | Session was canceled via `DELETE`. | Yes |
| `failed` | An unrecoverable error occurred. | Yes |
## Overrides
Reuse a catalog agent but adjust it for a single run with `overrides`, a map on the [create-session](/computer-use-agents/sessions/create) body. Rather than defining a new agent, you point at fields of the resolved request. Each key is a dotted path, and its value replaces whatever that path resolves to, applied after `agent` is expanded from its catalog id.
* Dots walk into objects. `agent.instructions` sets behavior, `agent.model` swaps the serving model, and `agent.answer_format` pins a [structured answer](#structured-output).
* A `[field=value]` selector picks a list member. `agent.environments[kind=web]` selects the web environment, so `agent.environments[kind=web].start_url` sets just its start page and `agent.environments[kind=web].mode` switches how it reads the page.
* Values are type-checked. Each value must match the type of the field its path targets. An unknown path or a wrong type is rejected with `422` at creation, before the agent runs.
For example, send a catalog web-surfer to a chosen page instead of its default start URL:
```bash CLI theme={null}
hai run "Summarize the top discussion right now" \
--agent h/web-surfer-flash \
--override 'agent.environments[kind=web].start_url=https://news.ycombinator.com'
```
```bash cURL theme={null}
curl -X POST https://agp.eu.hcompany.ai/api/v2/sessions \
-H "Authorization: Bearer $HAI_API_KEY" -H "Content-Type: application/json" \
-d '{
"agent": "h/web-surfer-flash",
"messages": [{"type": "user_message", "message": "Summarize the top discussion right now"}],
"overrides": {"agent.environments[kind=web].start_url": "https://news.ycombinator.com"}
}'
```
```python Python theme={null}
session = client.sessions.create_session(
agent="h/web-surfer-flash",
messages="Summarize the top discussion right now",
overrides={"agent.environments[kind=web].start_url": "https://news.ycombinator.com"},
)
```
```typescript TypeScript theme={null}
const session = await client.sessions.createSession({
body: {
agent: "h/web-surfer-flash",
messages: "Summarize the top discussion right now",
overrides: { "agent.environments[kind=web].start_url": "https://news.ycombinator.com" },
},
});
```
## Structured output
By default the agent's answer is free-form text. Set an `answer_format` (a JSON Schema) on the agent, or pass a Pydantic / Zod schema to the SDKs, and the final answer comes back as typed, validated data instead. See [Structured output](/computer-use-agents/structured-output).
## Listing and filtering
[`GET /api/v2/sessions`](/computer-use-agents/sessions/list) pages through your sessions, newest first, with filters you can combine:
| Filter | Type | Description |
| ------------------------------------ | ------------------- | ---------------------------------------------------------------------------------- |
| `status` | string (repeatable) | Filter by session status (e.g. `?status=running&status=queued`). |
| `agent` | string (repeatable) | Filter by agent identifier (e.g. `h/web-surfer-flash`). |
| `group_id` | string | Filter by group: useful for multi-session workflows. |
| `parent_session_id` | string | Find [child sessions](/computer-use-agents/multi-agent) of a parent. |
| `schedule_id` | string | Sessions created by a [schedule](/computer-use-agents/schedules/overview)'s fires. |
| `search` | string | Case-insensitive match on the first message or answer. |
| `created_before` / `created_after` | string | Bound by creation time (ISO 8601). |
| `finished_before` / `finished_after` | string | Bound by finish time (ISO 8601). |
| `owner` | string | Access scope. Default: `me-in-organization`. |
```bash cURL theme={null}
curl "https://agp.eu.hcompany.ai/api/v2/sessions?status=running&agent=web-price-finder" \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
page = client.sessions.list_sessions(status=["running"], agent=["web-price-finder"])
for summary in page.items:
print(summary.id, summary.status)
```
```typescript TypeScript theme={null}
const page = await client.sessions.listSessions({
status: ["running"],
agent: ["web-price-finder"],
});
for (const summary of page.items) {
console.log(summary.id, summary.status);
}
```
Like every list endpoint, it returns a page envelope: `items` holds the resources, `page` echoes the page number you asked for, and `total` counts all matches. Responses don't echo `size` back, so track it yourself; there are more pages while `page * size < total`. This endpoint caps `size` at `100` and sorts by `-created_at` (newest first) unless you pass `sort=created_at`. See [List sessions](/computer-use-agents/sessions/list) for the full parameter reference.
## Endpoints
| Method | Path | Description |
| ----------------- | ------------------------------------------------ | ----------------------------------------------------------------- |
| `POST` | `/api/v2/sessions` | [Create a session](/computer-use-agents/sessions/create) |
| `GET` | `/api/v2/sessions` | [List sessions](/computer-use-agents/sessions/list) |
| `GET` | `/api/v2/sessions/{id}` | [Retrieve a session](/computer-use-agents/sessions/retrieve) |
| `GET` | `/api/v2/sessions/{id}/status` | [Get session status](/computer-use-agents/sessions/status) |
| `DELETE` | `/api/v2/sessions/{id}` | [Cancel a session](/computer-use-agents/sessions/cancel) |
| `POST` | `/api/v2/sessions/{id}/messages` | [Send a message](/computer-use-agents/sessions/send-messages) |
| `POST` | `/api/v2/sessions/{id}/tool_results` | [Send tool results](/computer-use-agents/sessions/tool-results) |
| `POST` | `/api/v2/sessions/{id}/pause` | [Pause a session](/computer-use-agents/sessions/pause) |
| `POST` | `/api/v2/sessions/{id}/resume` | [Resume a session](/computer-use-agents/sessions/resume) |
| `POST` | `/api/v2/sessions/{id}/force_answer` | [Force final answer](/computer-use-agents/sessions/force-answer) |
| `GET` | `/api/v2/sessions/{id}/changes` | [Long-poll for changes](/computer-use-agents/sessions/changes) |
| `GET` | `/api/v2/sessions/{id}/events` | [List events](/computer-use-agents/sessions/events) |
| `GET` | `/api/v2/sessions/quota` | [Get quota](/computer-use-agents/sessions/quota) |
| `POST` | `/api/v2/sessions/{id}/feedback` | [Submit session feedback](/computer-use-agents/sessions/feedback) |
| `POST` / `DELETE` | `/api/v2/sessions/{id}/share` | [Share / unshare a session](/computer-use-agents/sessions/share) |
| `GET` | `/api/v2/sessions/{id}/resources/{bucket}/{key}` | [Get a session resource](/computer-use-agents/sessions/resources) |
# Pause a session
Source: https://hub.hcompany.ai/computer-use-agents/sessions/pause
POST /api/v2/sessions/{id}/pause
Halt a running session without losing state.
Pauses a running session. The agent stops processing but its state is fully preserved. You can [resume](/computer-use-agents/sessions/resume) it later to continue from exactly where it left off.
**Returns** `202 Accepted`. The pause command is delivered asynchronously.
***
## Path parameters
The session ID.
***
## Examples
```bash cURL theme={null}
curl -X POST https://agp.eu.hcompany.ai/api/v2/sessions/$SESSION_ID/pause \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
client.sessions.pause_session(session_id)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
await client.sessions.pauseSession({ id: sessionId });
```
***
## Errors
| Status | Cause |
| ------ | ------------------------------------------------------------------------------------------------------------------------------- |
| `404` | Session not found, or you don't have access. |
| `409` | The session is [`queued`](/computer-use-agents/observe-and-steer#queued-sessions) (nothing is running yet) or already finished. |
# Get quota
Source: https://hub.hcompany.ai/computer-use-agents/sessions/quota
GET /api/v2/sessions/quota
Check your concurrent session quota.
Returns the caller's concurrency quota: how many sessions may run at once, and how many slots are free right now. `available: 0` doesn't mean creates fail; new sessions are [`queued`](/computer-use-agents/observe-and-steer#queued-sessions) by default and start as slots free up.
***
## Response
The level the limit applies at: `user` or `org`.
Maximum number of concurrent sessions allowed.
Number of sessions currently holding a slot (recently active and not in a terminal state). [`queued`](/computer-use-agents/observe-and-steer#queued-sessions) sessions hold no slot and are not counted.
Remaining concurrent slots (`limit - active`).
***
## Examples
```bash cURL theme={null}
curl https://agp.eu.hcompany.ai/api/v2/sessions/quota \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
quota = client.sessions.get_session_quota()
print(quota.available)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const quota = await client.sessions.getSessionQuota();
console.log(quota.available);
```
```json Response theme={null}
{
"scope": "user",
"limit": 10,
"active": 3,
"available": 7
}
```
# Get resource
Source: https://hub.hcompany.ai/computer-use-agents/sessions/resources
GET /api/v2/sessions/{id}/resources/{bucket}/{key}
Access session-owned resources like screenshots and files.
Returns a redirect to a presigned S3 URL for a session-owned resource (screenshots, downloaded files, etc.).
Auth is optional: supports public shares.
**Returns** `302 Redirect` to the presigned URL.
***
## Path parameters
The session ID.
The resource bucket (e.g., `screenshots`, `files`).
The resource key (path within the bucket).
***
## Examples
```bash cURL theme={null}
curl -L https://agp.eu.hcompany.ai/api/v2/sessions/$SESSION_ID/resources/screenshots/step-5.png \
-H "Authorization: Bearer $HAI_API_KEY" \
-o screenshot.png
```
```python Python theme={null}
from hai_agents import Client
client = Client()
client.sessions.get_session_resource(session_id, "screenshots", "step-5.png")
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
await client.sessions.getSessionResource({
id: sessionId,
bucket: "screenshots",
key: "step-5.png",
});
```
The cURL `-L` flag follows the `302` redirect to download the file from S3.
# Resume a session
Source: https://hub.hcompany.ai/computer-use-agents/sessions/resume
POST /api/v2/sessions/{id}/resume
Continue a paused session.
Resumes a previously paused session. The agent picks up exactly where it left off.
**Returns** `202 Accepted`. The resume command is delivered asynchronously.
***
## Path parameters
The session ID.
***
## Examples
```bash cURL theme={null}
curl -X POST https://agp.eu.hcompany.ai/api/v2/sessions/$SESSION_ID/resume \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
client.sessions.resume_session(session_id)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
await client.sessions.resumeSession({ id: sessionId });
```
***
## Notes
* Resuming a session that is already `running` is accepted (`202`) and safe.
* You can also resume a paused session implicitly by [sending a message](/computer-use-agents/sessions/send-messages): the platform auto-resumes before delivering the message. This applies to `paused` sessions only; messages to a [`queued`](/computer-use-agents/observe-and-steer#queued-sessions) session are buffered until it starts.
***
## Errors
| Status | Cause |
| ------ | ------------------------------------------------------------------------------------------------------------------------------- |
| `404` | Session not found, or you don't have access. |
| `409` | The session is [`queued`](/computer-use-agents/observe-and-steer#queued-sessions) (nothing is running yet) or already finished. |
# Retrieve a session
Source: https://hub.hcompany.ai/computer-use-agents/sessions/retrieve
GET /api/v2/sessions/{id}
Get full details of a single session.
Retrieves the complete [Session](/computer-use-agents/sessions/overview) object, including the original request, current status, execution metadata, and `latest_answer` (the agent's final answer once produced). This is the cursor-independent way to read a finished run's result, with no event loop to drain.
Auth is optional, so public shares are supported.
**Returns** the [Session](/computer-use-agents/sessions/overview) object if the ID is valid and you have access. Returns `404` otherwise.
***
## Path parameters
The session ID returned when the session was created.
***
## Examples
```bash CLI theme={null}
hai sessions get a1b2c3d4-e5f6-7890-abcd-ef1234567890
```
```bash cURL theme={null}
curl https://agp.eu.hcompany.ai/api/v2/sessions/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
session = client.sessions.get_session("a1b2c3d4-e5f6-7890-abcd-ef1234567890")
print(session.status.status, session.latest_answer)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const session = await client.sessions.getSession({
id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
});
console.log(session.status.status, session.latestAnswer);
```
```json Response theme={null}
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"request": {
"agent": "h/web-surfer-flash",
"messages": [
{"type": "user_message", "message": "Find the top 3 stories on Hacker News"}
]
},
"status": {
"status": "completed",
"error": null,
"steps": 12,
"usage_per_model": [
{
"name": "holo3-1-35b-a3b",
"input_tokens": 24800,
"output_tokens": 1420,
"reasoning_tokens": 0
}
],
"subagent_session_ids": []
},
"agent_view_url": "https://platform.hcompany.ai/agents/sessions/a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"latest_answer": "1. ... 2. ... 3. ...",
"created_at": "2026-05-07T14:30:00Z",
"started_at": "2026-05-07T14:30:02Z",
"finished_at": "2026-05-07T14:31:15Z"
}
```
***
## Errors
| Status | Cause |
| ------ | -------------------------------------------------- |
| `404` | Session not found, or you don't have access to it. |
If you only need to know whether the session has finished, poll the lighter, faster [`GET /sessions/{id}/status`](/computer-use-agents/sessions/status) instead.
# Send messages
Source: https://hub.hcompany.ai/computer-use-agents/sessions/send-messages
POST /api/v2/sessions/{id}/messages
Chat with a running agent.
Sends one or more messages to an agent session: additional instructions, answers to the agent's questions, or a redirection of the task.
**Returns** `202 Accepted`. The message is delivered asynchronously: the agent processes it on its next reasoning step.
Messages adapt to the session's state rather than failing. A `paused` session is auto-resumed before delivery, a [`queued`](/computer-use-agents/observe-and-steer#queued-sessions) session buffers the message until it starts, and a finished session is restarted (back to `pending`, subject to your [token budget](/computer-use-agents/plans-and-limits#token-usage)) with the message delivered to the relaunched agent.
***
## Path parameters
The session ID.
***
## Request body
The body is a discriminated union: send either a single message or a batch.
### Single message
`"user_message"`. May be omitted.
The message content.
Optional list of base64 data URIs to attach to the message (e.g. `data:image/png;base64,...`).
Identifies the message sender. Defaults to `user`; leave it unset for normal user input.
### Batch
Must be `"batch"`.
Array of message objects, each with `type: "user_message"` and `message`. Processed in order.
***
## Examples
### Send a single message
```bash CLI theme={null}
hai sessions send "$SESSION_ID" 'Focus on hotels near Kiyomizu-dera temple, under $200/night'
```
```bash cURL theme={null}
curl -X POST https://agp.eu.hcompany.ai/api/v2/sessions/$SESSION_ID/messages \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "user_message",
"message": "Focus on hotels near Kiyomizu-dera temple, under $200/night"
}'
```
```python Python theme={null}
from hai_agents import Client
from hai_agents.sessions import SendSessionMessagesRequestBody_UserMessage
client = Client()
client.sessions.send_session_messages(
session_id,
request=SendSessionMessagesRequestBody_UserMessage(
message="Focus on hotels near Kiyomizu-dera temple, under $200/night",
),
)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
await client.sessions.sendSessionMessages({
id: sessionId,
body: {
type: "user_message",
message: "Focus on hotels near Kiyomizu-dera temple, under $200/night",
},
});
```
### Send a batch of messages
```bash cURL theme={null}
curl -X POST https://agp.eu.hcompany.ai/api/v2/sessions/$SESSION_ID/messages \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "batch",
"messages": [
{"type": "user_message", "message": "Also check availability for June 15-20"},
{"type": "user_message", "message": "I prefer traditional ryokans over modern hotels"}
]
}'
```
```python Python theme={null}
from hai_agents import Client, UserMessageEvent
from hai_agents.sessions import SendSessionMessagesRequestBody_Batch
client = Client()
client.sessions.send_session_messages(
session_id,
request=SendSessionMessagesRequestBody_Batch(
messages=[
UserMessageEvent(message="Also check availability for June 15-20"),
UserMessageEvent(message="I prefer traditional ryokans over modern hotels"),
],
),
)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
await client.sessions.sendSessionMessages({
id: sessionId,
body: {
type: "batch",
messages: [
{ type: "user_message", message: "Also check availability for June 15-20" },
{ type: "user_message", message: "I prefer traditional ryokans over modern hotels" },
],
},
});
```
***
## Errors
| Status | Cause |
| ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `402` | The message would restart a finished session, but your organization's token budget is exhausted. See [Plans and limits](/computer-use-agents/plans-and-limits#token-usage). |
| `404` | Session not found, or you don't have access. |
| `422` | Invalid message body, for example an image that isn't a decodable base64 data URI. |
# Share a session
Source: https://hub.hcompany.ai/computer-use-agents/sessions/share
POST /api/v2/sessions/{id}/share
Make a session publicly accessible or revoke access.
Make a session publicly accessible via a share URL. Anyone with the link can view the session without authentication.
***
## Make public
**`POST /api/v2/sessions/{id}/share`**
**Returns** `200` with a `ShareLink` object.
### Path parameters
The session ID.
### Response
```json Response theme={null}
{
"share_url": "/share/api/v1/trajectories/{id}"
}
```
`share_url` is a path relative to the API host you called, so prepend your regional base URL (for example `https://agp.eu.hcompany.ai`) before handing it out.
### Example
```bash CLI theme={null}
hai sessions share "$SESSION_ID"
```
```bash cURL theme={null}
curl -X POST https://agp.eu.hcompany.ai/api/v2/sessions/$SESSION_ID/share \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
link = client.sessions.share_session(session_id)
print(link.share_url)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const link = await client.sessions.shareSession({ id: sessionId });
console.log(link.shareUrl);
```
***
## Revoke public access
**`DELETE /api/v2/sessions/{id}/share`**
Revokes the public share link. The session is no longer accessible without authentication.
**Returns** `204 No Content`.
### Example
```bash cURL theme={null}
curl -X DELETE https://agp.eu.hcompany.ai/api/v2/sessions/$SESSION_ID/share \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
client.sessions.unshare_session(session_id)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
await client.sessions.unshareSession({ id: sessionId });
```
# Get session status
Source: https://hub.hcompany.ai/computer-use-agents/sessions/status
GET /api/v2/sessions/{id}/status
Lightweight polling endpoint for session progress.
Returns only the live status of a session: current state, step count, token usage, and subagent IDs. It's the cheapest call for a quick liveness check. To follow a run and read its `answer`, long-poll [`changes`](/computer-use-agents/sessions/changes) instead.
**Returns** a status object with the fields below.
***
## Path parameters
The session ID.
***
## Response
Current session state: `queued`, `pending`, `running`, `paused`, `idle`, `awaiting_tool_results`, `completed`, `failed`, `timed_out`, or `interrupted`.
Short, stable error message if the session failed or timed out. `null` otherwise. Branch on `error_code`, not on this text.
Machine-readable failure category if the session failed or timed out: `environment_error`, `no_answer`, `answer_validation`, `timeout`, or `internal`. `null` otherwise. See [Read how the run ended](/computer-use-agents/observe-and-steer#read-how-the-run-ended).
The agent's self-assessed task outcome, reported with its final answer: `success`, `partial`, `infeasible`, or `blocked`. `null` until reported. See [Read how the run ended](/computer-use-agents/observe-and-steer#outcomes).
Number of steps the agent has taken, where each step is one decide-and-act cycle.
Per-model token usage. Each entry is an object with `name`, `input_tokens`, `output_tokens`, and `reasoning_tokens`. Empty array until the agent calls a model.
```json theme={null}
"usage_per_model": [
{
"name": "holo3-1-35b-a3b",
"input_tokens": 12400,
"output_tokens": 890,
"reasoning_tokens": 0
}
]
```
IDs of the child sessions this session spawned, empty if it ran no subagents. Pull the full roster, each child labeled with its `agent`, with [`GET /sessions?parent_session_id={id}`](/computer-use-agents/sessions/list#find-the-subagents-a-session-spawned).
***
## Examples
```bash cURL theme={null}
curl https://agp.eu.hcompany.ai/api/v2/sessions/$SESSION_ID/status \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
status = client.sessions.get_session_status(session_id)
print(status.status, status.steps)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const status = await client.sessions.getSessionStatus({ id: sessionId });
console.log(status.status, status.steps);
```
```json Response theme={null}
{
"status": "running",
"error": null,
"error_code": null,
"outcome": null,
"steps": 7,
"usage_per_model": [
{
"name": "holo3-1-35b-a3b",
"input_tokens": 12400,
"output_tokens": 890,
"reasoning_tokens": 0
}
],
"subagent_session_ids": []
}
```
***
## Polling pattern
A typical polling loop checks status every few seconds and branches on the result:
```python Python theme={null}
import time
while True:
status = client.sessions.get_session_status(session_id)
match status.status:
case "completed":
# status carries no answer; read the snapshot off the session
session = client.sessions.get_session(session_id)
print(f"Done in {status.steps} steps: {session.latest_answer}")
break
case "failed" | "timed_out" | "interrupted":
print(f"Session ended: {status.status}")
if status.error:
print(f" Error: {status.error}")
break
case _:
print(f" {status.status}... ({status.steps} steps)")
time.sleep(3)
```
```typescript TypeScript theme={null}
while (true) {
const status = await client.sessions.getSessionStatus({ id: sessionId });
if (status.status === "completed") {
// status carries no answer; read the snapshot off the session
const session = await client.sessions.getSession({ id: sessionId });
console.log(`Done in ${status.steps} steps: ${session.latestAnswer}`);
break;
}
if (["failed", "timed_out", "interrupted"].includes(status.status)) {
console.log(`Session ended: ${status.status}`, status.error ?? "");
break;
}
console.log(` ${status.status}... (${status.steps} steps)`);
await new Promise((resolve) => setTimeout(resolve, 3000));
}
```
For the polling interval, 2 to 5 seconds works well for most use cases. For longer tasks (10+ minutes), back off to 10 to 15 seconds to reduce API calls.
# Send tool results
Source: https://hub.hcompany.ai/computer-use-agents/sessions/tool-results
POST /api/v2/sessions/{id}/tool_results
Answer the agent's pending custom tool calls.
Sends results for pending [custom tool](/computer-use-agents/custom-tools) calls. When the agent calls a custom tool, the session waits on `awaiting_tool_results`; posting a result for every pending call lets the run continue. The SDK run helpers call this endpoint for you.
Results are relayed to the agent as-is: the API doesn't check `tool_req.id` against the pending calls, so echo the request faithfully. Settling a call on a `paused` session leaves it paused; [resume](/computer-use-agents/sessions/resume) it separately.
**Returns** `202 Accepted`. The result is delivered asynchronously: the agent resumes once every pending call has one.
***
## Path parameters
The session ID.
***
## Request body
Send either a single settled call or a batch. A single call is a `tool_result` on success or an `error_event` on failure, discriminated by `kind`, and a batch wraps a list of them. Each call echoes back the full pending `tool_req` from [`pending_tool_calls`](/computer-use-agents/custom-tools) (`{ tool_name, args, id }`) rather than only its id.
### Tool result (success)
Must be `"tool_result"`.
The pending tool call this answers, echoed back from `pending_tool_calls`: `{ tool_name, args, id }`.
JSON-serializable tool output, shown to the model.
### Tool error (failure)
Must be `"error_event"`.
Error text shown to the model.
Component that produced the error, e.g. `"custom_tools"`.
The pending tool call this answers, echoed back from `pending_tool_calls`.
### Batch
Must be `"batch"`.
Array of `tool_result` and/or `error_event` objects.
***
## Examples
### Send a single result
```bash cURL theme={null}
curl -X POST https://agp.eu.hcompany.ai/api/v2/sessions/$SESSION_ID/tool_results \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"kind": "tool_result",
"tool_req": { "tool_name": "lookup_order", "args": { "order_id": "A1" }, "id": "call_1" },
"result": "shipped"
}'
```
```python Python theme={null}
from hai_agents import Client, ToolRequest, ToolResultEvent
client = Client()
client.sessions.send_session_tool_results(
session_id,
request=ToolResultEvent(
tool_req=ToolRequest(tool_name="lookup_order", args={"order_id": "A1"}, id="call_1"),
result="shipped",
),
)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
await client.sessions.sendSessionToolResults({
id: sessionId,
body: {
kind: "tool_result",
toolReq: { toolName: "lookup_order", args: { order_id: "A1" }, id: "call_1" },
result: "shipped",
},
});
```
### Send a batch of results
```bash cURL theme={null}
curl -X POST https://agp.eu.hcompany.ai/api/v2/sessions/$SESSION_ID/tool_results \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "batch",
"results": [
{"kind": "tool_result", "tool_req": {"tool_name": "lookup_order", "args": {}, "id": "call_1"}, "result": "shipped"},
{"kind": "error_event", "error": "Order not found", "origin": "custom_tools", "tool_req": {"tool_name": "lookup_order", "args": {}, "id": "call_2"}}
]
}'
```
```python Python theme={null}
from hai_agents import Client, ErrorEvent, ToolRequest, ToolResultBatch, ToolResultEvent
client = Client()
client.sessions.send_session_tool_results(
session_id,
request=ToolResultBatch(
results=[
ToolResultEvent(
tool_req=ToolRequest(tool_name="lookup_order", args={}, id="call_1"),
result="shipped",
),
ErrorEvent(
error="Order not found",
origin="custom_tools",
tool_req=ToolRequest(tool_name="lookup_order", args={}, id="call_2"),
),
],
),
)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
await client.sessions.sendSessionToolResults({
id: sessionId,
body: {
type: "batch",
results: [
{
kind: "tool_result",
toolReq: { toolName: "lookup_order", args: {}, id: "call_1" },
result: "shipped",
},
{
kind: "error_event",
error: "Order not found",
origin: "custom_tools",
toolReq: { toolName: "lookup_order", args: {}, id: "call_2" },
},
],
},
});
```
***
## Errors
| Status | Cause |
| ------ | ------------------------------------------------------------------------- |
| `404` | Session not found, or you don't have access. |
| `409` | The session already finished; its pending calls were resolved at run end. |
| `422` | Malformed request body. |
# Create a skill
Source: https://hub.hcompany.ai/computer-use-agents/skills/create
POST /api/v2/skills
Create a reusable skill in your catalog.
Creates a new skill in your catalog. Returns `201`.
**Returns** the created [Skill](/computer-use-agents/skills/overview) object.
***
## Request body
The body is the [Skill](/computer-use-agents/skills/overview) object. See that page for the full meaning of each field; the constraints that matter when creating one are below.
Catalog identifier, kebab-case with an optional single `org/` namespace prefix. The `h/` prefix is reserved for H's catalog (rejected with `403`) and marks the skill as reserved; any other name creates a custom skill, private to your organization. Immutable after creation.
One-line routing hint used for discovery. Non-empty.
The Markdown prompt fragment the agent receives at runtime. Non-empty.
Optional provenance URL.
Optional, informational regex hinting at URLs where this skill applies. 1 to 1024 characters.
***
## Examples
```bash cURL theme={null}
curl -X POST https://agp.eu.hcompany.ai/api/v2/skills \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "extract-table-data",
"description": "Extract structured data from HTML tables into JSON.",
"body": "When you encounter an HTML table, extract all rows and columns into a JSON array of objects. Each object should use the table headers as keys.",
"source": "https://github.com/myorg/skills"
}'
```
```python Python theme={null}
from hai_agents import Client
client = Client()
skill = client.skills.create_skill(
name="extract-table-data",
description="Extract structured data from HTML tables into JSON.",
body="When you encounter an HTML table, extract all rows and columns into a JSON array of objects. Each object should use the table headers as keys.",
source="https://github.com/myorg/skills",
)
print(skill.name)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const skill = await client.skills.createSkill({
name: "extract-table-data",
description: "Extract structured data from HTML tables into JSON.",
body: "When you encounter an HTML table, extract all rows and columns into a JSON array of objects. Each object should use the table headers as keys.",
source: "https://github.com/myorg/skills",
});
console.log(skill.name);
```
```json Response theme={null}
{
"name": "extract-table-data",
"description": "Extract structured data from HTML tables into JSON.",
"body": "When you encounter an HTML table...",
"source": "https://github.com/myorg/skills",
"url_pattern": null
}
```
***
## Errors
| Status | Cause |
| ------ | ------------------------------------------------------------------------ |
| `403` | Attempted to use the reserved `h/` namespace. |
| `409` | A skill with this name already exists in your catalog. |
| `422` | Body fails validation; common cases: empty `body`, invalid `name` shape. |
# Delete a skill
Source: https://hub.hcompany.ai/computer-use-agents/skills/delete
DELETE /api/v2/skills/{name}
Remove a skill from the catalog.
Removes a skill from your catalog. Agents that reference this skill will no longer have access to it in future sessions.
**Returns** `204 No Content` on success.
***
## Path parameters
The skill's `name` (e.g. `extract-table-data` or `myorg/web-helper`). Slash-containing names are supported.
***
## Examples
```bash cURL theme={null}
curl -X DELETE https://agp.eu.hcompany.ai/api/v2/skills/extract-table-data \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
client.skills.delete_skill(name="extract-table-data")
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
await client.skills.deleteSkill({ name: "extract-table-data" });
```
***
## Errors
| Status | Cause |
| ------ | ------------------------------------------- |
| `403` | The skill is reserved (`h/`) and read-only. |
| `404` | Skill not found or you don't have access. |
# List skills
Source: https://hub.hcompany.ai/computer-use-agents/skills/list
GET /api/v2/skills
Browse your skill catalog.
Returns a paginated list of skills in your catalog.
**Returns** a paginated list of [Skill](/computer-use-agents/skills/overview) objects.
***
## Query parameters
Page number (1-based).
Items per page. Maximum: `1000`.
Sort order. Options: `created_at`, `-created_at`, `name`, `-name`.
Case-insensitive substring match on the skill name.
***
## Examples
```bash cURL theme={null}
curl "https://agp.eu.hcompany.ai/api/v2/skills" \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
page = client.skills.list_skills()
for skill in page.items:
print(skill.name)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const page = await client.skills.listSkills();
for (const skill of page.items) {
console.log(skill.name);
}
```
Each item is a full [Skill](/computer-use-agents/skills/overview) object.
```json Response theme={null}
{
"items": [
{
"name": "extract-table-data",
"description": "Extract structured data from HTML tables into JSON.",
"body": "When you encounter an HTML table, extract all rows and columns into a JSON array of objects...",
"source": null,
"url_pattern": null
}
],
"page": 1,
"total": 5
}
```
# Skills
Source: https://hub.hcompany.ai/computer-use-agents/skills/overview
A skill is a reusable instruction fragment an agent can draw on.
A **skill** is a named Markdown fragment (an instruction or workflow) that an agent can load during a session. Use one of H's [built-in skills](#built-in-skills) or create your own, then attach it to any agent by `name`.
An agent loads a skill's full `body` only when its `description` looks relevant to the task at hand; the `description` is the trigger it [routes on](#how-an-agent-uses-a-skill).
## What's in a skill
| Field | Required | Description |
| ------------- | -------- | ----------------------------------------------------------------------------------------------------- |
| `name` | Yes | Identifies the skill in your catalog and is how agents reference it. |
| `description` | Yes | Tells the agent when to use the skill: the trigger it routes on to decide whether to load the `body`. |
| `body` | Yes | The Markdown instructions the agent loads when it uses the skill. |
| `source` | No | Records where the content came from. |
| `url_pattern` | No | Hints at URLs where the skill applies. Informational, not matched against the live page. |
For exact field constraints, see [Create a skill](/computer-use-agents/skills/create).
## Create custom skills
Create a skill once, then reference it by `name` from any agent's `skills` array.
```bash cURL theme={null}
curl -X POST https://agp.eu.hcompany.ai/api/v2/skills \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "extract-table-data",
"description": "Extract structured data from HTML tables into JSON.",
"body": "When you encounter an HTML table, extract all rows and columns into a JSON array of objects, using the headers as keys."
}'
```
```python Python theme={null}
from hai_agents import Client
client = Client()
client.skills.create_skill(
name="extract-table-data",
description="Extract structured data from HTML tables into JSON.",
body=(
"When you encounter an HTML table, extract all rows and columns into a JSON "
"array of objects, using the headers as keys."
),
)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
await client.skills.createSkill({
name: "extract-table-data",
description: "Extract structured data from HTML tables into JSON.",
body:
"When you encounter an HTML table, extract all rows and columns into a JSON " +
"array of objects, using the headers as keys.",
});
```
Reference it by `name` from the agent's `skills` array when you create the agent:
```bash cURL theme={null}
curl -X POST https://agp.eu.hcompany.ai/api/v2/agents \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "table-scraper",
"description": "Scrapes structured data from web tables.",
"environments": ["h/browser"],
"skills": ["extract-table-data"]
}'
```
```python Python theme={null}
client.agents.create_agent(
name="table-scraper",
description="Scrapes structured data from web tables.",
environments=["h/browser"],
skills=["extract-table-data"],
)
```
```typescript TypeScript theme={null}
await client.agents.createAgent({
name: "table-scraper",
description: "Scrapes structured data from web tables.",
environments: ["h/browser"],
skills: ["extract-table-data"],
});
```
## How an agent uses a skill
An agent doesn't read every attached skill up front. Each one is advertised to it by `name` and `description` in an `` section of the system prompt, with the `body` held back:
```xml theme={null}
extract-table-data
Extract structured data from HTML tables into JSON.
```
When the agent judges a skill's `description` relevant to the task, it calls a built-in `load_skill` tool with that `name`. The tool returns the full skill, and its `body` enters the conversation as instructions for the steps that follow. If the name doesn't match, the tool replies with the closest available names so the agent can retry. A `url_pattern`, when set, is listed next to the description as an extra hint; it is not matched against the live page.
[Environments](/computer-use-agents/environments/overview) ship with their own skills too, bundled by default and always in context rather than loaded on demand. These are fixed per environment, not something you configure.
## Write effective skills
* Make the `description` a trigger, not a summary. It is the only thing the agent sees before loading a skill, so phrase it as "use this when…" and name the situation precisely. Vague descriptions get loaded at the wrong time or not at all.
* Keep the `body` self-contained. The agent reads it cold, mid-task, so write a focused procedure (steps, formats, edge cases) rather than background prose.
* Put always-on behavior in the agent's [`instructions`](/computer-use-agents/agents/overview). Reserve skills for procedures that only apply some of the time, so they stay out of context until they are relevant.
* Compose small skills. Several narrow skills route better than one broad skill, because each `description` can target a distinct situation.
## Built-in skills
H maintains a catalog of computer-use skills under the `h/` namespace. Attach them to any agent by `name`.
List them by filtering on the `h/` prefix, or fetch one by name:
```bash cURL theme={null}
# List H-maintained skills
curl "https://agp.eu.hcompany.ai/api/v2/skills?name=h/" -H "Authorization: Bearer $HAI_API_KEY"
# Fetch one by name
curl "https://agp.eu.hcompany.ai/api/v2/skills/h/{skill_name}" -H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
# List H-maintained skills
skills = client.skills.list_skills(name="h/")
# Fetch one by name
skill = client.skills.get_skill("h/{skill_name}")
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
// List H-maintained skills
const skills = await client.skills.listSkills({ name: "h/" });
// Fetch one by name
const skill = await client.skills.getSkill({ name: "h/{skill_name}" });
```
## Endpoints
| Method | Path | Description |
| -------- | ----------------------- | -------------------------------------------------------- |
| `POST` | `/api/v2/skills` | [Create a skill](/computer-use-agents/skills/create) |
| `GET` | `/api/v2/skills` | [List skills](/computer-use-agents/skills/list) |
| `GET` | `/api/v2/skills/{name}` | [Retrieve a skill](/computer-use-agents/skills/retrieve) |
| `PUT` | `/api/v2/skills/{name}` | [Update a skill](/computer-use-agents/skills/update) |
| `PATCH` | `/api/v2/skills/{name}` | [Patch a skill](/computer-use-agents/skills/patch) |
| `DELETE` | `/api/v2/skills/{name}` | [Delete a skill](/computer-use-agents/skills/delete) |
The list is paginated (`page`, `size`) and returns an `items` / `page` / `total` envelope; sort it with `sort=created_at` or `sort=name` (prefix `-` for descending), and filter with `name` (substring match) or `search` (matches name or description).
# Patch a skill
Source: https://hub.hcompany.ai/computer-use-agents/skills/patch
PATCH /api/v2/skills/{name}
Change individual fields of a skill without resending the full object.
Partial update: only the fields you send change, everything else is preserved. Send a field as `null` to clear it. The merged result is validated like a [full update](/computer-use-agents/skills/update), and `name` is not patchable (renames are not supported).
**Returns** the updated [Skill](/computer-use-agents/skills/overview) object.
***
## Path parameters
The skill's `name` (e.g. `extract-table-data` or `myorg/web-helper`). Slash-containing names are supported.
***
## Request body
Any subset of the [Skill](/computer-use-agents/skills/overview) object's fields except `name`: `description`, `body`, `source`, `url_pattern`.
***
## Examples
Change the description and nothing else:
```bash cURL theme={null}
curl -X PATCH https://agp.eu.hcompany.ai/api/v2/skills/extract-table-data \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"description": "Extract data from HTML tables, with CSV output support."}'
```
```python Python theme={null}
from hai_agents import Client
client = Client()
skill = client.skills.patch_skill(
name="extract-table-data",
description="Extract data from HTML tables, with CSV output support.",
)
print(skill.description)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const skill = await client.skills.patchSkill({
name: "extract-table-data",
description: "Extract data from HTML tables, with CSV output support.",
});
console.log(skill.description);
```
***
## Errors
| Status | Cause |
| ------ | ----------------------------------------------------------------------------------------------- |
| `403` | The skill is reserved (`h/`) and read-only. |
| `404` | Skill not found or you don't have access. |
| `422` | The merged spec fails validation; common cases: `description` or `body` set to `null` or empty. |
# Retrieve a skill
Source: https://hub.hcompany.ai/computer-use-agents/skills/retrieve
GET /api/v2/skills/{name}
Get the full details of a skill.
Retrieves the complete [Skill](/computer-use-agents/skills/overview) object.
**Returns** the Skill object if the name is valid and you have access.
***
## Path parameters
The skill's `name` (e.g. `extract-table-data` or `myorg/web-helper`). Slash-containing names are supported.
***
## Examples
```bash cURL theme={null}
curl "https://agp.eu.hcompany.ai/api/v2/skills/extract-table-data" \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
skill = client.skills.get_skill(name="extract-table-data")
print(skill.body)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const skill = await client.skills.getSkill({ name: "extract-table-data" });
console.log(skill.body);
```
```json Response theme={null}
{
"name": "extract-table-data",
"description": "Extract structured data from HTML tables into JSON.",
"body": "When you encounter an HTML table, extract all rows and columns into a JSON array of objects, using the headers as keys.",
"source": null,
"url_pattern": null
}
```
***
## Errors
| Status | Cause |
| ------ | ----------------------------------------- |
| `404` | Skill not found or you don't have access. |
# Update a skill
Source: https://hub.hcompany.ai/computer-use-agents/skills/update
PUT /api/v2/skills/{name}
Edit a skill's content.
Updates an existing skill. This is a full replacement of the [Skill](/computer-use-agents/skills/overview) object. The `name` must match the URL identifier: renames are not supported.
**Returns** the updated [Skill](/computer-use-agents/skills/overview) object.
***
## Path parameters
The skill's `name` (e.g. `extract-table-data` or `myorg/web-helper`). Slash-containing names are supported.
***
## Request body
A full replacement of the [Skill](/computer-use-agents/skills/overview) object. The `name` in the body must equal the URL identifier. Any field you omit is reset to its default, not preserved. To change individual fields without resending the rest, use [Patch](/computer-use-agents/skills/patch) instead.
```json Request body theme={null}
{
"name": "extract-table-data",
"description": "Extract data from HTML tables, with CSV output support.",
"body": "When you encounter an HTML table, extract rows into JSON or CSV based on the user request.",
"source": "https://github.com/myorg/skills",
"url_pattern": null
}
```
***
## Examples
```bash cURL theme={null}
curl -X PUT https://agp.eu.hcompany.ai/api/v2/skills/extract-table-data \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "extract-table-data",
"description": "Extract data from HTML tables: updated with CSV output support.",
"body": "When you encounter an HTML table, extract all rows into JSON or CSV format based on the user request."
}'
```
```python Python theme={null}
from hai_agents import Client
client = Client()
skill = client.skills.update_skill(
name_="extract-table-data",
name="extract-table-data",
description="Extract data from HTML tables: updated with CSV output support.",
body="When you encounter an HTML table, extract all rows into JSON or CSV format based on the user request.",
)
print(skill.description)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const skill = await client.skills.updateSkill({
name: "extract-table-data",
body: {
name: "extract-table-data",
description: "Extract data from HTML tables: updated with CSV output support.",
body: "When you encounter an HTML table, extract all rows into JSON or CSV format based on the user request.",
},
});
console.log(skill.description);
```
```json Response theme={null}
{
"name": "extract-table-data",
"description": "Extract data from HTML tables: updated with CSV output support.",
"body": "When you encounter an HTML table, extract all rows into JSON or CSV format based on the user request.",
"source": null,
"url_pattern": null
}
```
***
## Errors
| Status | Cause |
| ------ | ------------------------------------------------------------------------------------- |
| `400` | The `name` in the body does not match the URL identifier (renames are not supported). |
| `403` | The skill is reserved (`h/`) and read-only. |
| `404` | Skill not found or you don't have access. |
| `422` | Body fails validation; common cases: empty `body`. |
# Get typed answers
Source: https://hub.hcompany.ai/computer-use-agents/structured-output
Get the agent's final answer as typed, schema-validated data instead of free-form text.
By default the agent's answer is free-form text. Set an [`answer_format`](/computer-use-agents/agents/overview) (a JSON Schema) on the agent and it returns data that conforms to it: the `answer` you read from [`changes`](/computer-use-agents/sessions/changes) is then a JSON object instead of a string. Define it inline on a custom agent, or ask a catalog agent for it on a single run with [`overrides`](/computer-use-agents/sessions/overview#overrides).
The SDKs go further: pass a [Pydantic](https://docs.pydantic.dev) model (Python) or [Zod v4](https://zod.dev) schema (TypeScript) as `answer_schema` / `answerSchema` and the SDK derives the JSON Schema for you, then parses the final answer back into a validated, typed instance. A `completed` session whose answer is missing or doesn't match the schema raises `AnswerValidationError` with the raw payload attached; the raw wire value always stays on the result's `final_changes` / `finalChanges`, next to the parsed answer.
The schema and an `agent.answer_format` override are two ways to set the same field, so passing both is rejected. Runs that end without reaching `completed`, such as an `idle` session that hasn't answered yet or a failed one, skip validation: the answer passes through as-is, and is `None` / `undefined` when absent.
```bash CLI theme={null}
# `hai run` prints the structured answer once it lands
hai run "Top 3 stories on Hacker News right now?" \
--agent h/web-surfer-flash \
--override 'agent.answer_format={"type":"object","properties":{"stories":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string"},"url":{"type":"string"}},"required":["title","url"]}}},"required":["stories"]}'
```
```bash cURL theme={null}
# Launch with a schema...
SESSION=$(curl -sX POST https://agp.eu.hcompany.ai/api/v2/sessions \
-H "Authorization: Bearer $HAI_API_KEY" -H "Content-Type: application/json" \
-d '{
"agent": "h/web-surfer-flash",
"messages": [{"type": "user_message", "message": "Top 3 stories on Hacker News right now?"}],
"overrides": {
"agent.answer_format": {
"type": "object",
"properties": {
"stories": {
"type": "array",
"items": {
"type": "object",
"properties": {"title": {"type": "string"}, "url": {"type": "string"}},
"required": ["title", "url"]
}
}
},
"required": ["stories"]
}
}
}' | jq -r .id)
# ...then read the structured answer once it lands.
curl -s "https://agp.eu.hcompany.ai/api/v2/sessions/$SESSION/changes" \
-H "Authorization: Bearer $HAI_API_KEY" | jq .answer
```
```python Python theme={null}
from pydantic import BaseModel
from hai_agents import Client
class Story(BaseModel):
title: str
url: str
class Stories(BaseModel):
stories: list[Story]
client = Client()
result = client.run_session(
agent="h/web-surfer-flash",
messages="Top 3 stories on Hacker News right now?",
answer_schema=Stories,
)
for story in result.answer.stories: # result.answer is a Stories instance
print(story.title, story.url)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
import { z } from "zod";
const Stories = z.object({
stories: z.array(z.object({ title: z.string(), url: z.string() })),
});
const client = new HaiAgentsClient();
const result = await client.runSession({
agent: "h/web-surfer-flash",
messages: "Top 3 stories on Hacker News right now?",
answerSchema: Stories,
});
for (const story of result.answer?.stories ?? []) {
console.log(story.title, story.url); // typed via z.infer
}
```
## Chaining agents
With a typed answer, an agent behaves like any other function: call it, get data back, build on it. Here one agent gathers sources and others read them in parallel:
```python Python theme={null}
import asyncio
from pydantic import BaseModel
from hai_agents import AsyncClient
class Source(BaseModel):
title: str
url: str
excerpt: str
class Sources(BaseModel):
sources: list[Source]
class Brief(BaseModel):
url: str
summary: str
key_facts: list[str]
async def main() -> None:
client = AsyncClient()
scout = await client.run_session(
agent="h/web-surfer-flash",
messages="Find the 5 highest-value sources on EU AI Act enforcement",
answer_schema=Sources,
)
readers = await asyncio.gather(*(
client.run_session(
agent="h/web-surfer-flash",
messages=f"Read this source and extract the key facts: {source.url}",
overrides={"agent.environments[kind=web].start_url": source.url},
answer_schema=Brief,
)
for source in scout.answer.sources
))
for reader in readers:
print(reader.answer.url, reader.answer.key_facts)
asyncio.run(main())
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
import { z } from "zod";
const Sources = z.object({
sources: z.array(z.object({ title: z.string(), url: z.string(), excerpt: z.string() })),
});
const Brief = z.object({
url: z.string(),
summary: z.string(),
keyFacts: z.array(z.string()),
});
const client = new HaiAgentsClient();
const scout = await client.runSession({
agent: "h/web-surfer-flash",
messages: "Find the 5 highest-value sources on EU AI Act enforcement",
answerSchema: Sources,
});
const readers = await Promise.all(
(scout.answer?.sources ?? []).map((source) =>
client.runSession({
agent: "h/web-surfer-flash",
messages: `Read this source and extract the key facts: ${source.url}`,
overrides: { "agent.environments[kind=web].start_url": source.url },
answerSchema: Brief,
}),
),
);
for (const reader of readers) {
console.log(reader.answer?.url, reader.answer?.keyFacts);
}
```
Parallel sessions count against your [concurrency quota](/computer-use-agents/sessions/quota).
# Handle two-factor authentication
Source: https://hub.hcompany.ai/computer-use-agents/two-factor-auth
When a login or signup asks for a one-time password or confirmation link, let the agent request it through a prebuilt custom tool.
Sites that protect a login with email codes, SMS codes, or confirmation links send a value the agent cannot invent. The SDKs ship a prebuilt [custom tool](/computer-use-agents/custom-tools) for that moment: the agent calls `request_otp`, your process resolves the code or link, and the run continues with the single value.
Pass `otp_tool` / `otpTool` in `tools` the same way you would any other custom tool. Without a handler it prompts on stdin; with a handler you can read an inbox over IMAP or fetch the value from anywhere else your code can reach.
## Prompt interactively
The default handler is enough for local runs: when the agent hits a 2FA step, your terminal asks for the code or link.
```python Python theme={null}
from hai_agents import Client
from hai_agents_tools import otp_tool
client = Client()
result = client.run_session(
agent="h/web-surfer-flash",
messages="Log in to example.com and summarize the inbox.",
tools=[otp_tool()],
)
print(result.answer)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient, otpTool } from "hai-agents";
const client = new HaiAgentsClient();
const result = await client.runSession({
agent: "h/web-surfer-flash",
messages: "Log in to example.com and summarize the inbox.",
tools: [otpTool()],
});
console.log(result.answer);
```
## Read the code from email
For unattended runs, hand the tool an IMAP handler. It polls unread mail (newest first), extracts a code or confirmation link, marks that message read so a retry cannot reuse a stale code, and returns only that value to the agent. For Gmail or Google Workspace, use an [app password](https://support.google.com/accounts/answer/185833).
```python Python theme={null}
import os
from hai_agents import Client
from hai_agents_tools import imap_otp_handler, otp_tool
handler = imap_otp_handler(
host="imap.gmail.com",
username="agent-inbox@gmail.com",
password=os.environ["GMAIL_APP_PASSWORD"],
sender="no-reply@example.com", # optional: only mail from this address
)
client = Client()
result = client.run_session(
agent="h/web-surfer-flash",
messages="Log in to example.com and check for new notifications.",
tools=[otp_tool(handler)],
)
print(result.answer)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient, imapOtpHandler, otpTool } from "hai-agents";
// Optional deps for the IMAP handler: npm install imapflow mailparser
const handler = imapOtpHandler({
host: "imap.gmail.com",
username: "agent-inbox@gmail.com",
password: process.env.GMAIL_APP_PASSWORD!,
sender: "no-reply@example.com", // optional: only mail from this address
});
const client = new HaiAgentsClient();
const result = await client.runSession({
agent: "h/web-surfer-flash",
messages: "Log in to example.com and check for new notifications.",
tools: [otpTool({ handler })],
});
console.log(result.answer);
```
Like every custom tool, the handler runs in your process: IMAP credentials never leave your machine, and the agent only receives the extracted code or link — never mailbox contents, subjects, or senders.
Useful IMAP options:
| Option | Default | Role |
| ------------------------------ | ------------------- | ------------------------------------------------------------------------- |
| `sender` | unset | Only consider messages from this address |
| `timeout_s` / `timeoutMs` | 2 minutes | Give up (tool error to the agent) after this long |
| `max_age_s` / `maxAgeMs` | 15 minutes | Ignore unread mail older than this |
| `code_pattern` / `codePattern` | built-in heuristics | Override extraction; first capture group (or the whole match) is the code |
## Supply a custom handler
Any function that takes the agent's request and returns a string works: prompt in Slack, call an inbox API, read SMS from a provider, and so on. Handlers may be sync or async.
```python Python theme={null}
from hai_agents_tools import OtpRequest, otp_tool
def from_slack(request: OtpRequest) -> str:
# request.prompt, request.kind ("code" | "link"), request.source
return slack.ask_user(request.prompt)
tools = [otp_tool(from_slack)]
```
```typescript TypeScript theme={null}
import { otpTool, type OtpRequest } from "hai-agents";
async function fromSlack(request: OtpRequest): Promise {
// request.prompt, request.kind ("code" | "link"), request.source
return slack.askUser(request.prompt);
}
const tools = [otpTool({ handler: fromSlack })];
```
## What the agent sends
The tool's input schema is fixed. The agent fills:
| Field | Required | Meaning |
| -------- | -------- | ----------------------------------------------------------------------------- |
| `prompt` | yes | Human-readable ask, e.g. "Enter the 6-digit code sent to j\*\*\*@example.com" |
| `kind` | no | `"code"` (default) or `"link"` for a full confirmation URL |
| `source` | no | Where it was sent, e.g. `"email"`, `"sms"`, `"authenticator app"` |
Your handler should return a non-empty string. Empty values fail as a tool error so the agent can retry or stop cleanly.
## Authenticator apps via a vault
If the site uses a TOTP authenticator and the secret already lives in [1Password](https://developer.1password.com/), bind a [vault](/computer-use-agents/vaults/overview) to the browser instead. When the page matches a stored item, the session offers [`fill_secret_at`](/computer-use-agents/browser/configuration#actions) with `totp` and injects the code without putting it in the agent's context. Reach for `otp_tool` when the code arrives out of band (email, SMS, magic link); reach for a vault when the TOTP secret is already in your secrets provider.
# Create a vault
Source: https://hub.hcompany.ai/computer-use-agents/vaults/create
POST /api/v2/vaults
Register a secrets provider for your organization.
Registers a [vault](/computer-use-agents/vaults/overview) (a link between your organization and an external secrets provider) so an agent can sign in to the sites it works on without you passing the secrets through the API. Today the only provider is [1Password](https://developer.1password.com/): you record which 1Password vault to read (`op_vault_id`) and a [service account token](https://www.1password.dev/service-accounts) that grants access to it.
The token is validated against the provider before it is stored, and is never returned by any endpoint.
Returns `201` with the created vault object (see [Retrieve](/computer-use-agents/vaults/retrieve) for the full field list).
The request body carries a plaintext service account token. Send it only over HTTPS and never log it.
***
## Request body
Human-readable label for the config.
Provider settings.
* `provider` (string, optional): Secrets provider. Defaults to `onepassword`, the only supported value.
* `op_vault_id` (string, required): Identifier of the 1Password vault to read credentials from.
The 1Password service account token granting access to the vault. Write-only: validated before storage and omitted from every response.
***
## Examples
```bash cURL theme={null}
curl -X POST https://agp.eu.hcompany.ai/api/v2/vaults \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "prod-1password",
"provider_config": {"provider": "onepassword", "op_vault_id": "abcd1234efgh5678"},
"token": "ops_eyJ..."
}'
```
```python Python theme={null}
from hai_agents import Client, OnePasswordConfig
client = Client()
vault = client.vaults.create_vault(
name="prod-1password",
provider_config=OnePasswordConfig(op_vault_id="abcd1234efgh5678"),
token="ops_eyJ...",
)
print(vault.id)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const vault = await client.vaults.createVault({
name: "prod-1password",
providerConfig: { opVaultId: "abcd1234efgh5678" },
token: "ops_eyJ...",
});
console.log(vault.id);
```
```json Response theme={null}
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"org_id": "1c9a2f6e-4b3d-4a8c-9e5f-7d6b8a0c1e2f",
"name": "prod-1password",
"provider_config": {"provider": "onepassword", "op_vault_id": "abcd1234efgh5678"},
"created_at": "2026-05-07T14:30:00Z",
"updated_at": "2026-05-07T14:30:00Z"
}
```
***
## Errors
| Status | Cause |
| ------ | ----------------------------------------------------------------------------------------------- |
| `409` | A vault with this `name` already exists in your organization. Names are unique per org. |
| `422` | Body failed validation, or the provider rejected the token (it could not access `op_vault_id`). |
# Delete a vault
Source: https://hub.hcompany.ai/computer-use-agents/vaults/delete
DELETE /api/v2/vaults/{vault_id}
Remove a vault config.
Deletes a vault config. Agents in your organization can no longer read credentials through it in future runs.
Returns `204 No Content` on success.
***
## Path parameters
The vault config's `id` (UUID).
***
## Examples
```bash cURL theme={null}
curl -X DELETE https://agp.eu.hcompany.ai/api/v2/vaults/f47ac10b-58cc-4372-a567-0e02b2c3d479 \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
client.vaults.delete_vault(vault_id="f47ac10b-58cc-4372-a567-0e02b2c3d479")
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
await client.vaults.deleteVault({ vaultId: "f47ac10b-58cc-4372-a567-0e02b2c3d479" });
```
***
## Errors
| Status | Cause |
| ------ | ----------------------------------------- |
| `404` | Vault not found or you don't have access. |
# Check health
Source: https://hub.hcompany.ai/computer-use-agents/vaults/health
GET /api/v2/vaults/{vault_id}/health
Probe a vault's provider connection.
Probes the provider behind a vault config to confirm the stored token still works. Use it before a run that depends on the vault, since a token can be revoked or expire on the provider side.
The endpoint returns `200` whenever the provider is reachable, so branch on the `ok` field, not the HTTP status.
Returns a health object with `ok` and an optional `error`.
***
## Path parameters
The vault config's `id` (UUID).
***
## Response
`true` if the provider accepted the stored token and the vault is reachable.
Short reason when `ok` is `false`; `null` otherwise.
***
## Examples
```bash cURL theme={null}
curl "https://agp.eu.hcompany.ai/api/v2/vaults/f47ac10b-58cc-4372-a567-0e02b2c3d479/health" \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
health = client.vaults.vault_health(vault_id="f47ac10b-58cc-4372-a567-0e02b2c3d479")
if not health.ok:
print("vault unhealthy:", health.error)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const health = await client.vaults.vaultHealth({ vaultId: "f47ac10b-58cc-4372-a567-0e02b2c3d479" });
if (!health.ok) {
console.log("vault unhealthy:", health.error);
}
```
```json Response theme={null}
{
"ok": true,
"error": null
}
```
***
## Errors
| Status | Cause |
| ------ | ----------------------------------------- |
| `404` | Vault not found or you don't have access. |
# List vaults
Source: https://hub.hcompany.ai/computer-use-agents/vaults/list
GET /api/v2/vaults
Browse your organization's vault configs.
Returns the vault configs owned by your organization, with offset-based pagination.
Returns an object with `total`, `limit`, `offset`, and a `vaults` array of [vault objects](/computer-use-agents/vaults/retrieve).
***
## Query parameters
Maximum number of configs to return. Between `1` and `1000`.
Number of configs to skip before collecting the page.
***
## Examples
```bash cURL theme={null}
curl "https://agp.eu.hcompany.ai/api/v2/vaults?limit=50&offset=0" \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
page = client.vaults.list_vaults(limit=50, offset=0)
for vault in page.vaults:
print(vault.id, vault.name)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const page = await client.vaults.listVaults({ limit: 50, offset: 0 });
for (const vault of page.vaults) {
console.log(vault.id, vault.name);
}
```
```json Response theme={null}
{
"total": 1,
"limit": 50,
"offset": 0,
"vaults": [
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"org_id": "1c9a2f6e-4b3d-4a8c-9e5f-7d6b8a0c1e2f",
"name": "prod-1password",
"provider_config": {"provider": "onepassword", "op_vault_id": "abcd1234efgh5678"},
"created_at": "2026-05-07T14:30:00Z",
"updated_at": "2026-05-07T14:30:00Z"
}
]
}
```
# Vaults
Source: https://hub.hcompany.ai/computer-use-agents/vaults/overview
Let an agent sign in to the sites and services it works on, without passing secrets through the API.
When an agent needs to sign in to a site or service, you don't pass the credentials through the API. Instead you register a vault (a link between your organization and an external secrets provider), and the agent pulls the right secret at the moment it needs it. Secrets never travel through your API requests and are never returned by any endpoint.
Today the only provider is [1Password](https://developer.1password.com/).
## What a vault stores
A vault config records two things:
* `op_vault_id`: which 1Password vault to read credentials from.
* A [service account token](https://www.1password.dev/service-accounts) authorizing access to that vault.
Manage vault configs through the [Vaults](/computer-use-agents/vaults/create) endpoints: create, list, retrieve, update, rotate the token, delete, and health-check.
## Set up a vault
Creating a vault is not enough on its own: nothing uses it until you bind it to a [Browser](/computer-use-agents/browser/configuration) environment.
Register the 1Password vault and a [service account token](https://www.1password.dev/service-accounts) that grants access to it. The token is validated against the provider, kept write-only, and never returned. See [Create a vault](/computer-use-agents/vaults/create) for the full field list.
```bash cURL theme={null}
curl -X POST https://agp.eu.hcompany.ai/api/v2/vaults \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "prod-1password",
"provider_config": {"provider": "onepassword", "op_vault_id": "abcd1234efgh5678"},
"token": "ops_eyJ..."
}'
```
```python Python theme={null}
from hai_agents import Client, OnePasswordConfig
client = Client()
vault = client.vaults.create_vault(
name="prod-1password",
provider_config=OnePasswordConfig(op_vault_id="abcd1234efgh5678"),
token="ops_eyJ...",
)
print(vault.id)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const vault = await client.vaults.createVault({
name: "prod-1password",
providerConfig: { opVaultId: "abcd1234efgh5678" },
token: "ops_eyJ...",
});
console.log(vault.id);
```
Set the browser's `vault_id` to the vault's `id`, inline in an agent's `environments` list or on a catalog [environment](/computer-use-agents/environments/create). The vault must belong to your organization.
Vaults are only supported on cloud-hosted browsers (`host: "cloud"`, the default). Secrets are resolved and typed inside H Company infrastructure and never leave it, so a browser running on your own device cannot bind a vault; setting `vault_id` with `host: "user_device"` is rejected.
```bash cURL theme={null}
curl -X POST https://agp.eu.hcompany.ai/api/v2/environments \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"id": "signed-in-browser",
"kind": "web",
"vault_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
}'
```
```python Python theme={null}
client.environments.create_environment(
id="signed-in-browser",
kind="web",
vault_id="f47ac10b-58cc-4372-a567-0e02b2c3d479",
)
```
```typescript TypeScript theme={null}
await client.environments.createEnvironment({
id: "signed-in-browser",
kind: "web",
vaultId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
});
```
Run a session against an agent using that browser as usual. Whenever a credential in the vault matches the page the agent is on, the session offers it a [`fill_secret_at`](/computer-use-agents/browser/configuration#actions) action to sign in. Leave `vault_id` unset to run without secret access.
## How secrets are matched
Agents never name a 1Password item directly. When an agent fills a credential, the item is selected automatically from the page's URL:
* Domain gating: an item is eligible only if the page's hostname equals, or is a subdomain of, one of the item's stored sites. An agent on `test.hcompany.ai` can use an item stored for `hcompany.ai` (a parent domain), but not one stored for `prod.hcompany.ai` (a sibling).
* Closest host wins: when several eligible items hold the same field, the one whose hostname matches the page most specifically is chosen. An exact host beats a parent-domain match, and a deeper subdomain beats a shallower one.
* Path breaks ties: if two items match the hostname equally well, the one whose stored path best prefixes the page URL wins. Path is only a tiebreaker, never a requirement.
Store one item per site so matching stays unambiguous.
# Retrieve a vault
Source: https://hub.hcompany.ai/computer-use-agents/vaults/retrieve
GET /api/v2/vaults/{vault_id}
Fetch a vault config by id.
Fetches a single vault config. The stored service account token is write-only and is never included in the response.
Returns the vault object.
***
## Path parameters
The vault config's `id` (UUID).
***
## The vault object
Unique vault config identifier (UUID).
Organization that owns the config (UUID).
Human-readable label.
Provider settings.
* `provider` (string): Secrets provider. Currently always `onepassword`.
* `op_vault_id` (string): Identifier of the 1Password vault credentials are read from.
ISO 8601 creation timestamp.
ISO 8601 timestamp of the last change.
***
## Examples
```bash cURL theme={null}
curl "https://agp.eu.hcompany.ai/api/v2/vaults/f47ac10b-58cc-4372-a567-0e02b2c3d479" \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
vault = client.vaults.get_vault(vault_id="f47ac10b-58cc-4372-a567-0e02b2c3d479")
print(vault.name, vault.provider_config.op_vault_id)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const vault = await client.vaults.getVault({ vaultId: "f47ac10b-58cc-4372-a567-0e02b2c3d479" });
console.log(vault.name, vault.providerConfig.opVaultId);
```
```json Response theme={null}
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"org_id": "1c9a2f6e-4b3d-4a8c-9e5f-7d6b8a0c1e2f",
"name": "prod-1password",
"provider_config": {"provider": "onepassword", "op_vault_id": "abcd1234efgh5678"},
"created_at": "2026-05-07T14:30:00Z",
"updated_at": "2026-05-07T14:30:00Z"
}
```
***
## Errors
| Status | Cause |
| ------ | ----------------------------------------- |
| `404` | Vault not found or you don't have access. |
# Rotate the token
Source: https://hub.hcompany.ai/computer-use-agents/vaults/rotate-token
PUT /api/v2/vaults/{vault_id}/token
Replace a vault's stored service account token.
Replaces the service account token stored for a vault config. The new token is health-checked against the provider before it is written, so a token that cannot reach `op_vault_id` is rejected and the old one stays in place.
Returns `204 No Content` on success.
The request body carries a plaintext service account token. Send it only over HTTPS, never log it, and note that rotation is not idempotent: a retry after a 5xx may apply twice.
***
## Path parameters
The vault config's `id` (UUID).
***
## Request body
The new 1Password service account token. Write-only and never returned.
***
## Examples
```bash cURL theme={null}
curl -X PUT https://agp.eu.hcompany.ai/api/v2/vaults/f47ac10b-58cc-4372-a567-0e02b2c3d479/token \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"token": "ops_newtoken..."}'
```
```python Python theme={null}
from hai_agents import Client
client = Client()
client.vaults.rotate_vault_token(
vault_id="f47ac10b-58cc-4372-a567-0e02b2c3d479",
token="ops_newtoken...",
)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
await client.vaults.rotateVaultToken({
vaultId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
token: "ops_newtoken...",
});
```
***
## Errors
| Status | Cause |
| ------ | ------------------------------------------------------------------- |
| `404` | Vault not found or you don't have access. |
| `422` | The provider rejected the new token. The stored token is unchanged. |
# Update a vault
Source: https://hub.hcompany.ai/computer-use-agents/vaults/update
PATCH /api/v2/vaults/{vault_id}
Change a vault config's name or provider settings.
Partially updates a vault config. Only the fields you send are changed. To replace the stored service account token, use [Rotate the token](/computer-use-agents/vaults/rotate-token) instead.
Returns the updated [vault object](/computer-use-agents/vaults/retrieve).
***
## Path parameters
The vault config's `id` (UUID).
***
## Request body
New label for the config.
Replacement provider settings: `provider` (optional, defaults `onepassword`) and `op_vault_id` (required when provided).
***
## Examples
```bash cURL theme={null}
curl -X PATCH https://agp.eu.hcompany.ai/api/v2/vaults/f47ac10b-58cc-4372-a567-0e02b2c3d479 \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "prod-1password-eu"}'
```
```python Python theme={null}
from hai_agents import Client
client = Client()
vault = client.vaults.update_vault(
vault_id="f47ac10b-58cc-4372-a567-0e02b2c3d479",
name="prod-1password-eu",
)
print(vault.name)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const vault = await client.vaults.updateVault({
vaultId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
name: "prod-1password-eu",
});
console.log(vault.name);
```
***
## Errors
| Status | Cause |
| ------ | ----------------------------------------- |
| `404` | Vault not found or you don't have access. |
| `422` | Body failed validation. |
# Create a webhook
Source: https://hub.hcompany.ai/computer-use-agents/webhooks/create
POST /api/v2/webhooks
Register a URL to receive signed event notifications.
Registers a [webhook](/computer-use-agents/webhooks/overview) for your organization. The response includes the signing `secret`. This is the only time it is returned, so store it securely.
**Returns** `201` with the created webhook object plus its `secret`.
The `secret` cannot be retrieved later. To replace it, use [Rotate](/computer-use-agents/webhooks/rotate).
***
## Request body
Target URL for deliveries. Must be `https://` and publicly reachable: the platform sends a `HEAD` request at creation time and rejects the webhook if the endpoint cannot be reached. Any HTTP status counts as reachable; your endpoint does not need to accept `HEAD`.
Event types delivered to this webhook. `"*"` subscribes to the `session.status_updated` firehose; granular `session.*` types are delivered only when listed explicitly. See the [event catalog](/computer-use-agents/webhooks/events) for supported types.
Optional label for the webhook (max 255 characters).
***
## Examples
```bash cURL theme={null}
curl -X POST https://agp.eu.hcompany.ai/api/v2/webhooks \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/hooks/h",
"enabled_events": ["session.status_updated"],
"description": "Production listener"
}'
```
```python Python theme={null}
from hai_agents import Client
client = Client()
webhook = client.webhooks.create_webhook(
url="https://example.com/hooks/h",
enabled_events=["session.status_updated"],
description="Production listener",
)
print(webhook.secret) # shown only once; store it securely
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const webhook = await client.webhooks.createWebhook({
url: "https://example.com/hooks/h",
enabledEvents: ["session.status_updated"],
description: "Production listener",
});
console.log(webhook.secret); // shown only once; store it securely
```
```json Response theme={null}
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"url": "https://example.com/hooks/h",
"enabled_events": ["session.status_updated"],
"description": "Production listener",
"disabled": false,
"last_delivery_status": null,
"last_delivery_error": null,
"last_delivery_at": null,
"last_success_at": null,
"consecutive_failures": 0,
"created_at": "2026-06-11T15:04:05Z",
"updated_at": "2026-06-11T15:04:05Z",
"secret": "whsec_k3TQyhq2mPv8WdJ4cN7xLbR9sF1aZ6uE0gYoHiC5jXw"
}
```
***
## Errors
| Status | Cause |
| ------ | ------------------------------------------------------------------------------------------------------------------------ |
| `400` | Organization webhook limit reached (10), URL does not resolve to a public address, or the endpoint could not be reached. |
| `422` | Body failed validation: non-`https` URL, empty or unknown `enabled_events`. |
# Delete a webhook
Source: https://hub.hcompany.ai/computer-use-agents/webhooks/delete
DELETE /api/v2/webhooks/{webhook_id}
Remove a webhook.
Deletes a webhook. Deliveries to its URL stop immediately.
**Returns** `204 No Content` on success.
***
## Path parameters
The webhook's `id` (UUID).
***
## Examples
```bash cURL theme={null}
curl -X DELETE https://agp.eu.hcompany.ai/api/v2/webhooks/f47ac10b-58cc-4372-a567-0e02b2c3d479 \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
client.webhooks.delete_webhook(webhook_id="f47ac10b-58cc-4372-a567-0e02b2c3d479")
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
await client.webhooks.deleteWebhook({ webhookId: "f47ac10b-58cc-4372-a567-0e02b2c3d479" });
```
***
## Errors
| Status | Cause |
| ------ | ------------------------------------------- |
| `404` | Webhook not found or you don't have access. |
# List event types
Source: https://hub.hcompany.ai/computer-use-agents/webhooks/events
GET /api/v2/webhooks/events
List the webhook event types you can subscribe to.
Lists every concrete event type that can appear in a webhook's `enabled_events`, with a human-readable description. Use it to populate subscription UIs or to discover types added after you integrated.
**Returns** an array of event type definitions.
***
## The event type definition object
The event type identifier, e.g. `session.completed`.
When the event is sent.
***
## Examples
```bash cURL theme={null}
curl https://agp.eu.hcompany.ai/api/v2/webhooks/events \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
for event in client.webhooks.list_webhook_events():
print(event.type, "-", event.description)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const events = await client.webhooks.listWebhookEvents();
for (const event of events) {
console.log(event.type, "-", event.description);
}
```
```json Response theme={null}
[
{
"type": "session.awaiting_tool_results",
"description": "Sent when the agent is waiting for client-side tool results."
},
{
"type": "session.completed",
"description": "Sent when a session finishes successfully."
},
{
"type": "session.failed",
"description": "Sent when a session fails."
},
{
"type": "session.idle",
"description": "Sent when the agent finishes a run and waits for the next message."
},
{
"type": "session.status_updated",
"description": "Sent on every session status change, including running, completed, failed, timed out, interrupted, paused, idle, or awaiting tool results."
},
{
"type": "session.timed_out",
"description": "Sent when a session exceeds its time limit."
}
]
```
# List webhooks
Source: https://hub.hcompany.ai/computer-use-agents/webhooks/list
GET /api/v2/webhooks
Browse your organization's webhooks.
Returns a paginated list of your organization's webhooks. Signing secrets are never included.
**Returns** a paginated list of [webhook objects](/computer-use-agents/webhooks/retrieve).
***
## Query parameters
Page number (1-based).
Items per page. Maximum: `1000`.
Sort order. Options: `created_at`, `-created_at`.
***
## Examples
```bash cURL theme={null}
curl "https://agp.eu.hcompany.ai/api/v2/webhooks" \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
page = client.webhooks.list_webhooks()
for webhook in page.items:
print(webhook.id, webhook.url)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const page = await client.webhooks.listWebhooks();
for (const webhook of page.items) {
console.log(webhook.id, webhook.url);
}
```
```json Response theme={null}
{
"items": [
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"url": "https://example.com/hooks/h",
"enabled_events": ["session.status_updated"],
"description": "Production listener",
"disabled": false,
"last_delivery_status": "succeeded",
"last_delivery_error": null,
"last_delivery_at": "2026-07-03T08:30:00Z",
"last_success_at": "2026-07-03T08:30:00Z",
"consecutive_failures": 0,
"created_at": "2026-06-11T15:04:05Z",
"updated_at": "2026-06-11T15:04:05Z"
}
],
"page": 1,
"total": 1
}
```
# Get notified with webhooks
Source: https://hub.hcompany.ai/computer-use-agents/webhooks/overview
Receive signed HTTP notifications when your sessions change status.
A webhook is an HTTPS URL you register for your organization. When a [session](/computer-use-agents/sessions/overview) changes status, the platform sends a signed `POST` request to every subscribed webhook, so you can react to completions and failures without polling.
Manage webhooks with the [CRUD API](/computer-use-agents/webhooks/create). Each one has a target `url`, a list of `enabled_events`, and a signing `secret` returned once at creation.
## Events
| Event type | Sent when |
| ------------------------------- | --------------------------------------------------------------------------------------- |
| `session.status_updated` | A session's status changes, e.g. `running` → `completed`. |
| `session.completed` | A session finishes successfully. |
| `session.failed` | A session fails. |
| `session.timed_out` | A session exceeds its time limit. |
| `session.idle` | The agent answered and is waiting for the next message. |
| `session.awaiting_tool_results` | The agent is waiting for [client-side tool results](/computer-use-agents/custom-tools). |
All event types share the same payload shape. `"*"` subscribes to the `session.status_updated` firehose. Each type in `enabled_events` is delivered independently: subscribing to both `session.status_updated` and `session.failed` gets you two deliveries when a session fails, one per type. List the available types programmatically with [List event types](/computer-use-agents/webhooks/events).
## Delivery payload
Each delivery is a `POST` with a JSON body:
```json Delivery payload theme={null}
{
"type": "session.status_updated",
"id": "evt_5d1f0c9e8a7b4c2da93f1e6b8c4d2a70",
"created_at": "2026-06-11T15:04:05.123Z",
"data": {
"session_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"status": "completed",
"previous_status": "running"
}
}
```
The event type, e.g. `session.status_updated`.
Unique id for this event.
When the status change occurred (UTC, RFC 3339, millisecond precision).
Event payload: `session_id`, the new `status` (`queued`, `pending`, `running`, `paused`, `idle`, `awaiting_tool_results`, `completed`, `failed`, `timed_out`, or `interrupted`), and the `previous_status` it transitioned from (`null` when unknown).
## Verifying deliveries
Every delivery carries these headers:
| Header | Value |
| ----------------------- | -------------------------------------------------------------------------------------------- |
| `X-H-Webhook-Timestamp` | Unix timestamp (seconds) of the delivery attempt. |
| `X-H-Webhook-Signature` | `sha256=` + hex HMAC-SHA256 of `{timestamp}.{raw_body}`, keyed with your webhook's `secret`. |
| `X-H-Webhook-Delivery` | Unique id for this delivery, stable across retries. |
Always verify before trusting a delivery. The SDKs ship a helper that checks the signature, rejects stale deliveries (older than 5 minutes by default), and parses the event.
```python Python (FastAPI) theme={null}
from fastapi import FastAPI, Header, HTTPException, Request
from hai_agents import WebhookEventData, WebhookVerificationError, verify_webhook
app = FastAPI()
@app.post("/hooks/h")
async def receive(
request: Request,
x_h_webhook_signature: str = Header(""),
x_h_webhook_timestamp: str = Header(""),
):
body = await request.body()
try:
event = verify_webhook(body, x_h_webhook_signature, x_h_webhook_timestamp, secret="whsec_...")
except WebhookVerificationError:
raise HTTPException(status_code=400, detail="invalid signature")
# event.data is the raw payload; its shape depends on event.type.
if event.type == "session.status_updated":
data = WebhookEventData.model_validate(event.data)
if data.status == "completed":
print(f"session {data.session_id} finished")
return {"ok": True}
```
```typescript TypeScript (Express) theme={null}
import express from "express";
import { type WebhookEventData, WebhookVerificationError, verifyWebhook } from "hai-agents";
const app = express();
app.use("/hooks/h", express.raw({ type: "application/json" }));
app.post("/hooks/h", (req, res) => {
try {
const event = verifyWebhook(
req.body,
req.header("X-H-Webhook-Signature") ?? "",
req.header("X-H-Webhook-Timestamp") ?? "",
"whsec_...",
);
// event.data is the raw payload; its shape depends on event.type.
if (event.type === "session.status_updated") {
const data = event.data as unknown as WebhookEventData;
if (data.status === "completed") {
console.log(`session ${data.session_id} finished`);
}
}
res.json({ ok: true });
} catch (e) {
if (e instanceof WebhookVerificationError) {
res.status(400).json({ error: "invalid signature" });
return;
}
throw e;
}
});
```
Verify against the raw request body bytes, exactly as received. Parsing and re-serializing the JSON changes the bytes and invalidates the signature.
To verify manually: compute `HMAC-SHA256(secret, "{timestamp}." + raw_body)`, hex-encode it, prefix with `sha256=`, and compare it to `X-H-Webhook-Signature` using a constant-time comparison. Reject deliveries whose timestamp is more than a few minutes old to guard against replays.
## Delivery semantics
Deliveries are at-least-once, with a 10-second timeout per attempt. If your endpoint is unreachable or returns a non-2xx status, delivery is retried with increasing backoff, up to 8 attempts spanning about 24 hours, after which the event is dropped.
Because of retries:
* **Deduplicate.** The same event can arrive more than once. The event `id` and the `X-H-Webhook-Delivery` header are stable across retries; skip ids you have already processed.
* **Ignore arrival order.** A retried old event can land after a newer one. Trust the event's own `status`, `created_at`, and `previous_status`, not the order of arrival.
* **Return 2xx quickly.** Any other status counts as a failure and schedules a retry. Do slow work after responding.
Webhooks are a trigger rather than a source of truth: on receipt, fetch the authoritative state with [Get session status](/computer-use-agents/sessions/status).
Each webhook records the result of its latest delivery attempt: [Retrieve](/computer-use-agents/webhooks/retrieve) returns `last_delivery_status`, `last_delivery_error`, `last_delivery_at`, `last_success_at`, and `consecutive_failures`, so you can check whether an endpoint is healthy, and why it was disabled, without digging through receiver logs.
## Testing an endpoint
[Ping](/computer-use-agents/webhooks/ping) sends a signed `ping` event through the real delivery path and returns your endpoint's HTTP response synchronously, so you can validate URL, signature verification, and connectivity before relying on the webhook.
## Rotating the secret
[Rotate](/computer-use-agents/webhooks/rotate) replaces the signing secret without a verification gap:
1. Deploy your receiver passing **both** the current and a placeholder for the new secret to the verify helper (it accepts a list).
2. Call the rotate endpoint and store the new secret; update the receiver's secret list.
3. Once deliveries verify against the new secret, remove the old one.
## Constraints
* Target URLs must be `https://` and resolve to a public address. Reachability is verified with a `HEAD` request when you register or change the URL, and deliveries to private or internal hosts fail (and count as failed attempts).
* The signing `secret` is returned only by [Create](/computer-use-agents/webhooks/create) and [Rotate](/computer-use-agents/webhooks/rotate).
* An organization can register up to 10 webhooks.
* A `disabled` webhook stays registered but receives no deliveries.
* After 50 consecutive failed delivery attempts, a webhook is automatically disabled. Fix the receiver, then re-enable it with [Update](/computer-use-agents/webhooks/update) (`{"disabled": false}`).
# Ping a webhook
Source: https://hub.hcompany.ai/computer-use-agents/webhooks/ping
POST /api/v2/webhooks/{webhook_id}/ping
Send a signed test event and see how your endpoint responds.
Sends a signed `ping` event to the webhook through the real delivery path (same signature headers, same URL safety checks) and returns your endpoint's HTTP response synchronously. Use it to validate connectivity and signature verification end to end before relying on the webhook.
The ping body has the standard event envelope with `"type": "ping"` and `"data": {"webhook_id": "..."}`. Pings bypass `enabled_events` filtering and are not retried.
**Returns** the receiving endpoint's status code.
***
## Path parameters
The webhook's `id` (UUID).
***
## Examples
```bash cURL theme={null}
curl -X POST https://agp.eu.hcompany.ai/api/v2/webhooks/f47ac10b-58cc-4372-a567-0e02b2c3d479/ping \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
result = client.webhooks.ping_webhook(webhook_id="f47ac10b-58cc-4372-a567-0e02b2c3d479")
print(result.response_status) # 200 if your endpoint accepted the ping
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const result = await client.webhooks.pingWebhook({ webhookId: "f47ac10b-58cc-4372-a567-0e02b2c3d479" });
console.log(result.responseStatus); // 200 if your endpoint accepted the ping
```
```json Response theme={null}
{
"response_status": 200
}
```
***
## Errors
| Status | Cause |
| ------ | -------------------------------------------------------------------------- |
| `400` | The webhook URL is not `https://` or does not resolve to a public address. |
| `404` | Webhook not found or you don't have access. |
| `502` | The endpoint could not be reached (connection error or timeout). |
# Retrieve a webhook
Source: https://hub.hcompany.ai/computer-use-agents/webhooks/retrieve
GET /api/v2/webhooks/{webhook_id}
Fetch a single webhook by id.
Fetches one webhook. The signing `secret` is never returned by reads; it appears only in the [Create](/computer-use-agents/webhooks/create) response.
**Returns** the webhook object.
***
## Path parameters
The webhook's `id` (UUID).
***
## The webhook object
Unique identifier (UUID).
Target URL for deliveries.
Event types delivered to this webhook; `["*"]` means the `session.status_updated` firehose.
Optional label.
When `true`, the webhook stays registered but receives no deliveries. Set manually via [Update](/computer-use-agents/webhooks/update), or automatically after repeated delivery failures.
Result of the most recent delivery attempt: `succeeded` or `failed`. `null` before the first attempt.
What went wrong on the most recent delivery attempt, e.g. `HTTP 503` or a connection error. `null` when it succeeded.
When the most recent delivery attempt happened (UTC). `null` before the first attempt.
When a delivery last succeeded (UTC). `null` if none has.
Failed delivery attempts since the last success. Resets to `0` on a successful delivery or a manual re-enable; the webhook is disabled automatically when it grows too large. See [Delivery semantics](/computer-use-agents/webhooks/overview#delivery-semantics).
Creation time (UTC).
Last modification time (UTC).
***
## Examples
```bash cURL theme={null}
curl https://agp.eu.hcompany.ai/api/v2/webhooks/f47ac10b-58cc-4372-a567-0e02b2c3d479 \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
webhook = client.webhooks.get_webhook(webhook_id="f47ac10b-58cc-4372-a567-0e02b2c3d479")
print(webhook.url, webhook.enabled_events)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const webhook = await client.webhooks.getWebhook({ webhookId: "f47ac10b-58cc-4372-a567-0e02b2c3d479" });
console.log(webhook.url, webhook.enabledEvents);
```
```json Response theme={null}
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"url": "https://example.com/hooks/h",
"enabled_events": ["session.status_updated"],
"description": "Production listener",
"disabled": false,
"last_delivery_status": "succeeded",
"last_delivery_error": null,
"last_delivery_at": "2026-07-03T08:30:00Z",
"last_success_at": "2026-07-03T08:30:00Z",
"consecutive_failures": 0,
"created_at": "2026-06-11T15:04:05Z",
"updated_at": "2026-06-11T15:04:05Z"
}
```
***
## Errors
| Status | Cause |
| ------ | ------------------------------------------- |
| `404` | Webhook not found or you don't have access. |
# Rotate a webhook secret
Source: https://hub.hcompany.ai/computer-use-agents/webhooks/rotate
POST /api/v2/webhooks/{webhook_id}/rotate
Replace the signing secret without missing a verification.
Replaces the webhook's signing secret. The response includes the new `secret`; like [Create](/computer-use-agents/webhooks/create), this is the only time it is returned, so store it securely. Deliveries are signed at send time, so events (including retries already in flight) are signed with the new secret from this point on.
**Returns** the webhook object plus its new `secret`.
Update your receiver to accept both the old and the new secret **before** calling this endpoint. The SDK verify helpers accept a list of secrets for exactly this overlap: `verify_webhook(body, sig, ts, ["whsec_old", "whsec_new"])`. Remove the old secret once deliveries verify against the new one.
***
## Path parameters
The webhook's `id` (UUID).
***
## Examples
```bash cURL theme={null}
curl -X POST https://agp.eu.hcompany.ai/api/v2/webhooks/f47ac10b-58cc-4372-a567-0e02b2c3d479/rotate \
-H "Authorization: Bearer $HAI_API_KEY"
```
```python Python theme={null}
from hai_agents import Client
client = Client()
webhook = client.webhooks.rotate_webhook_secret(webhook_id="f47ac10b-58cc-4372-a567-0e02b2c3d479")
print(webhook.secret) # store it now; it is never returned again
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const webhook = await client.webhooks.rotateWebhookSecret({ webhookId: "f47ac10b-58cc-4372-a567-0e02b2c3d479" });
console.log(webhook.secret); // store it now; it is never returned again
```
```json Response theme={null}
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"url": "https://example.com/hooks/h",
"enabled_events": ["*"],
"description": "Production listener",
"disabled": false,
"last_delivery_status": "succeeded",
"last_delivery_error": null,
"last_delivery_at": "2026-07-02T08:30:00Z",
"last_success_at": "2026-07-02T08:30:00Z",
"consecutive_failures": 0,
"created_at": "2026-06-11T15:04:05Z",
"updated_at": "2026-07-02T09:00:00Z",
"secret": "whsec_nZbY0eXaMpLeOnLyDoNotUse0aQ3rT5uV7wX9yZ1aB3c"
}
```
***
## Errors
| Status | Cause |
| ------ | ------------------------------------------- |
| `404` | Webhook not found or you don't have access. |
# Update a webhook
Source: https://hub.hcompany.ai/computer-use-agents/webhooks/update
PATCH /api/v2/webhooks/{webhook_id}
Change a webhook's URL, events, description, or disabled state.
Partially updates a webhook. Only the fields you send are changed. The signing `secret` cannot be changed here; use [Rotate](/computer-use-agents/webhooks/rotate).
**Returns** the updated [webhook object](/computer-use-agents/webhooks/retrieve).
***
## Path parameters
The webhook's `id` (UUID).
***
## Request body
New target URL. Must be `https://` and publicly reachable; it is verified with a `HEAD` request before the change is saved, and the old URL is kept if verification fails.
Replacement list of [event types](/computer-use-agents/webhooks/events), or `["*"]` for the `session.status_updated` firehose.
New label (max 255 characters).
Set `true` to pause deliveries without deleting the webhook. Setting `false` re-enables a webhook that was disabled automatically after repeated delivery failures.
***
## Examples
```bash cURL theme={null}
curl -X PATCH https://agp.eu.hcompany.ai/api/v2/webhooks/f47ac10b-58cc-4372-a567-0e02b2c3d479 \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"disabled": true}'
```
```python Python theme={null}
from hai_agents import Client
client = Client()
webhook = client.webhooks.update_webhook(
webhook_id="f47ac10b-58cc-4372-a567-0e02b2c3d479",
disabled=True,
)
print(webhook.disabled)
```
```typescript TypeScript theme={null}
import { HaiAgentsClient } from "hai-agents";
const client = new HaiAgentsClient();
const webhook = await client.webhooks.updateWebhook({
webhookId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
disabled: true,
});
console.log(webhook.disabled);
```
***
## Errors
| Status | Cause |
| ------ | --------------------------------------------------------------------------- |
| `400` | The new URL does not resolve to a public address or could not be reached. |
| `404` | Webhook not found or you don't have access. |
| `422` | Body failed validation: non-`https` URL, empty or unknown `enabled_events`. |
# Provide your agents access to the docs
Source: https://hub.hcompany.ai/docs-mcp-server
Connect Claude, Cursor, and other MCP clients to the H Tech Hub docs.
This documentation site ships a hosted [Model Context Protocol](https://modelcontextprotocol.io) server that covers **the documentation only**. Connect it to your AI tool and the tool can search and read every page on this site while it answers your questions — no stale training data, no copy-pasting pages into the context window. It cannot run agents, call the H API, or touch your account.
The server is public and requires no API key:
```text theme={null}
https://hub.hcompany.ai/mcp
```
Looking to **run and manage Computer-Use Agents** from an MCP host? That's the separate [Computer-Use Agents MCP server](/computer-use-agents/mcp) — it talks to the live API and needs your H API key.
## Add to your editor
One click installs the server; no credentials needed:
Opens Cursor and prompts to install.
Opens VS Code and prompts to install.
You can also copy the server URL from the contextual menu on any page of this site.
## Connect manually
```json Cursor theme={null}
// ~/.cursor/mcp.json
{
"mcpServers": {
"h-tech-hub-docs": {
"url": "https://hub.hcompany.ai/mcp"
}
}
}
```
```bash Claude Code theme={null}
claude mcp add --scope user --transport http h-tech-hub-docs https://hub.hcompany.ai/mcp
```
```json VS Code theme={null}
// user mcp.json (MCP: Open User Configuration)
{
"servers": {
"h-tech-hub-docs": {
"type": "http",
"url": "https://hub.hcompany.ai/mcp"
}
}
}
```
```toml Codex theme={null}
# ~/.codex/config.toml
[mcp_servers.h-tech-hub-docs]
url = "https://hub.hcompany.ai/mcp"
```
Clients that support MCP discovery can also find the server automatically via the discovery document at [`/.well-known/mcp`](https://hub.hcompany.ai/.well-known/mcp).
## Tools
| Tool | What it does |
| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `search_h_tech_hub` | Searches the site and returns relevant snippets with titles and links. |
| `query_docs_filesystem_h_tech_hub` | Runs read-only queries (`rg`, `cat`, `tree`, `jq`, ...) against a virtual filesystem containing every page and the OpenAPI specs. |
| `submit_feedback` | Reports incorrect, outdated, or confusing documentation to the docs team. |
The search and filesystem tools are read-only; the server has no access to your account, your sessions, or anything beyond the published content of this site.
## Other machine-readable formats
If your tool doesn't speak MCP, the same content is available as plain Markdown:
* [`/llms.txt`](https://hub.hcompany.ai/llms.txt) — an index of every page with links and descriptions.
* Append `.md` to any page URL to get its raw Markdown, e.g. [`/quickstart.md`](https://hub.hcompany.ai/quickstart.md).
# Document OCR
Source: https://hub.hcompany.ai/document-ocr
Pass Holo a document page as an image and get clean Markdown back: headings, lists, tables, and equations, in reading order. There is no dedicated OCR endpoint; it is the same OpenAI-compatible `chat/completions` call with an image plus a transcription prompt. Send it as a single request with `temperature=0.0` and `enable_thinking=False` so the model transcribes in one shot instead of reasoning first.
Holo OCR is strongest on **English, digitally generated documents** (exported PDFs, slides, web pages, reports). Scanned pages and photos are best-effort, and **handwriting is not a good fit**. For high-stakes handwritten or non-Latin content, use a dedicated OCR system.
Set up the OpenAI client first by following the [Quickstart](/quickstart).
## Transcribe a page
Send one page image and read the Markdown from `message.content`.
```python Python theme={null}
IMAGE_URL = "https://your-host/page.png" # or "data:image/png;base64,..."
OCR_PROMPT = (
"Transcribe this document page to Markdown, preserving the reading order, "
"headings, lists, and tables. Render tables as Markdown tables and equations "
"as LaTeX. Return only the transcription, with no commentary and no surrounding "
"code fence. If the page has no readable text, return an empty string."
)
response = client.chat.completions.create(
model="holo3-1-35b-a3b",
messages=[{
"role": "user",
"content": [
{"type": "image_url", "image_url": {"url": IMAGE_URL}},
{"type": "text", "text": OCR_PROMPT},
],
}],
temperature=0.0,
extra_body={"chat_template_kwargs": {"enable_thinking": False}},
)
print(response.choices[0].message.content)
```
```typescript TypeScript theme={null}
const IMAGE_URL = "https://your-host/page.png"; // or "data:image/png;base64,..."
const OCR_PROMPT =
"Transcribe this document page to Markdown, preserving the reading order, " +
"headings, lists, and tables. Render tables as Markdown tables and equations " +
"as LaTeX. Return only the transcription, with no commentary and no surrounding " +
"code fence. If the page has no readable text, return an empty string.";
const response = await client.chat.completions.create({
model: "holo3-1-35b-a3b",
messages: [
{
role: "user",
content: [
{ type: "image_url", image_url: { url: IMAGE_URL } },
{ type: "text", text: OCR_PROMPT },
],
},
],
temperature: 0.0,
// chat_template_kwargs is H-specific, passed through in the request body
...({ chat_template_kwargs: { enable_thinking: false } } as any),
});
console.log(response.choices[0].message.content);
```
## Multi-page PDFs
Holo reads images, not PDFs, so rasterize each page to an image and transcribe them one per request, then stitch the results. One page per request keeps each image at full resolution and is the most reliable pattern.
```python Python theme={null}
import base64
import pymupdf # pip install pymupdf
def ocr_page(png_bytes: bytes) -> str:
data_uri = "data:image/png;base64," + base64.b64encode(png_bytes).decode()
response = client.chat.completions.create(
model="holo3-1-35b-a3b",
messages=[{
"role": "user",
"content": [
{"type": "image_url", "image_url": {"url": data_uri}},
{"type": "text", "text": OCR_PROMPT},
],
}],
temperature=0.0,
extra_body={"chat_template_kwargs": {"enable_thinking": False}},
)
return response.choices[0].message.content or ""
with pymupdf.open("document.pdf") as doc:
pages = [ocr_page(page.get_pixmap(dpi=200).tobytes("png")) for page in doc]
markdown = "\n\n".join(pages)
print(markdown)
```
```typescript TypeScript theme={null}
import { pdf } from "pdf-to-img"; // npm install pdf-to-img
async function ocrPage(png: Buffer): Promise {
const dataUri = "data:image/png;base64," + png.toString("base64");
const response = await client.chat.completions.create({
model: "holo3-1-35b-a3b",
messages: [
{
role: "user",
content: [
{ type: "image_url", image_url: { url: dataUri } },
{ type: "text", text: OCR_PROMPT },
],
},
],
temperature: 0.0,
...({ chat_template_kwargs: { enable_thinking: false } } as any),
});
return response.choices[0].message.content ?? "";
}
const pages: string[] = [];
for await (const page of await pdf("document.pdf", { scale: 2 })) {
pages.push(await ocrPage(page));
}
const markdown = pages.join("\n\n");
console.log(markdown);
```
Rasterize at roughly 150 to 200 DPI (or `scale: 2`). Lower resolution loses small text; much higher wastes tokens without improving accuracy. Run pages concurrently to speed up long documents, within your [rate limit](/models#rate-limits-and-billing).
`holo3-1-35b-a3b` caps output at 4,096 tokens per request, and a dense page (large tables, small print) can exceed that: check `finish_reason` and treat `length` as a truncated transcription. Split the page image, or switch to `holo3-122b-a10b` (32,768-token output cap) for dense documents. See [Models](/models).
## Next steps
Get click coordinates from a screenshot.
How to use Holo in your computer-use harness.
Endpoint, models, parameters, and limits.
# Element localization
Source: https://hub.hcompany.ai/element-localization
Pass Holo a screenshot (URL or base64 data URI) and a text description of an element; get click coordinates back. Single-turn, no history, no thinking: set `temperature=0.0` and `enable_thinking=False`. It is a grounding primitive you can use as a vision tool inside any agent, and both Holo3 and Holo3.1 support it.
Set up the OpenAI client first by following the [Quickstart](/quickstart).
```python Python theme={null}
from pydantic import BaseModel, Field
MODEL_NAME = "holo3-1-35b-a3b"
SCREENSHOT_URL = "https://your-host/screenshot.png" # or "data:image/png;base64,..."
SCREENSHOT_WIDTH, SCREENSHOT_HEIGHT = 1280, 720
ELEMENT = "the 'Sign in' button in the top-right corner"
class VisualLocalizerOutput(BaseModel):
x: int = Field(ge=0, le=1000, description="X coordinate as integer in [0, 1000]")
y: int = Field(ge=0, le=1000, description="Y coordinate as integer in [0, 1000]")
schema = VisualLocalizerOutput.model_json_schema()
prompt = (
"Localize an element on the GUI image according to the provided target "
"and output a click position.\n"
f" * You must output a valid JSON following the format: {schema}\n"
f" Your target is:\n{ELEMENT}"
)
response = client.chat.completions.create(
model=MODEL_NAME,
messages=[{
"role": "user",
"content": [
{"type": "image_url", "image_url": {"url": SCREENSHOT_URL}},
{"type": "text", "text": prompt},
],
}],
extra_body={
"structured_outputs": {"json": schema},
"chat_template_kwargs": {"enable_thinking": False},
},
temperature=0.0,
)
point = VisualLocalizerOutput.model_validate_json(response.choices[0].message.content)
abs_x = int(point.x / 1000 * SCREENSHOT_WIDTH)
abs_y = int(point.y / 1000 * SCREENSHOT_HEIGHT)
print(f"Click at ({abs_x}, {abs_y})")
```
```typescript TypeScript theme={null}
const MODEL_NAME = "holo3-1-35b-a3b";
const SCREENSHOT_URL = "https://your-host/screenshot.png"; // or "data:image/png;base64,..."
const SCREENSHOT_WIDTH = 1280;
const SCREENSHOT_HEIGHT = 720;
const ELEMENT = "the 'Sign in' button in the top-right corner";
const schema = {
type: "object",
properties: {
x: { type: "integer", minimum: 0, maximum: 1000, description: "X coordinate as integer in [0, 1000]" },
y: { type: "integer", minimum: 0, maximum: 1000, description: "Y coordinate as integer in [0, 1000]" },
},
required: ["x", "y"],
};
const prompt =
"Localize an element on the GUI image according to the provided target " +
"and output a click position.\n" +
` * You must output a valid JSON following the format: ${JSON.stringify(schema)}\n` +
` Your target is:\n${ELEMENT}`;
const response = await client.chat.completions.create({
model: MODEL_NAME,
messages: [
{
role: "user",
content: [
{ type: "image_url", image_url: { url: SCREENSHOT_URL } },
{ type: "text", text: prompt },
],
},
],
temperature: 0.0,
// structured_outputs and chat_template_kwargs are H-specific, passed through in the request body
...({
structured_outputs: { json: schema },
chat_template_kwargs: { enable_thinking: false },
} as any),
});
const point = JSON.parse(response.choices[0].message.content!) as { x: number; y: number };
const absX = Math.round((point.x / 1000) * SCREENSHOT_WIDTH);
const absY = Math.round((point.y / 1000) * SCREENSHOT_HEIGHT);
console.log(`Click at (${absX}, ${absY})`);
```
Coordinates come back as integers in `[0, 1000]`, normalized to the image you sent. Scale them to pixels with the image's own dimensions, and send and scale against the same image bytes: any resize, crop, or DPI mismatch will misplace the point.
## Next steps
How to use Holo in your computer-use harness.
Endpoint, models, parameters, and limits.
Back to setup and your first call.
# Glossary
Source: https://hub.hcompany.ai/glossary
Key terms used across the Models API docs, grouped by theme.
## Models and families
| Term | Definition |
| :------------------- | :--------------------------------------------------------------------------------------------------------------------------------------- |
| Holo3.1 | Latest generation Vision-Language Model (VLM) family for GUI agents that interact with real digital environments (web, desktop, mobile). |
| Holo3.1 family | Model sizes from 0.8B to 35B-A3B, spanning on-device to server deployments. |
| Holo3.1-35B-A3B | Open-source (Apache 2.0) model variant, available in BF16, FP8, NVFP4, and Q4 GGUF for cloud and local inference. |
| Holo3 | Prior generation that Holo3.1 builds on. |
| Holo2 | Earlier generation model that Holo3 improved upon. |
| Qwen/Qwen3.5-35B-A3B | Base model used for fine-tuning Holo3.1-35B-A3B. |
| Surfer-H | Example computer-use agent built on the Holo model family. |
## Capabilities and tasks
| Term | Definition |
| :------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Vision-Language Model (VLM) | A model that understands both visual inputs (like UI screens) and text, so it can interpret interfaces and perform actions. |
| GUI Agents | AI agents that operate graphical user interfaces by observing screens, reasoning about them, and executing actions. |
| Computer Use (CU) | The ability of an AI system to perform tasks on a computer, such as navigating interfaces and executing commands. |
| Navigation (in AI agents) | The process of completing tasks through multi-step reasoning and actions across interfaces. |
| Element Localization | Single-turn vision task: given a screenshot and a text description of a target UI element, return click coordinates. A grounding primitive that can be used inside larger agent harnesses. |
| Action Grounding | Connecting model decisions to actual executable actions in an environment. |
| Cross-environment Generalization | Ability to perform well across different platforms (web, desktop, mobile), including unseen environments. |
## API concepts
| Term | Definition |
| :---------------------- | :------------------------------------------------------------------------------------------------------------------------------ |
| Structured outputs | Decoding-level constraint that forces the response to be a JSON object matching a schema you pass in `structured_outputs.json`. |
| Native function calling | OpenAI-style `tools` / `tool_calls` interface, supported by `holo3-1-35b-a3b`. |
| Reasoning channel | The thinking trace returned in `message.reasoning` when `enable_thinking` is on; dropped between turns by the chat template. |
| Image budget | The practice of keeping at most the last 3 screenshots in context for best accuracy. |
| Coordinate convention | Click positions returned as integers in `[0, 1000]`, normalized to the image you sent, origin top-left. |
## Benchmarks
| Term | Definition |
| :-------------------- | :-------------------------------------------------------------------- |
| OSWorld | Benchmark evaluating performance in real Ubuntu desktop environments. |
| WebVoyager / WebArena | Benchmarks for testing web navigation and task completion abilities. |
| AndroidWorld | Benchmark for evaluating performance on mobile environments. |
# Architecture
Source: https://hub.hcompany.ai/holo-desktop-cli/architecture
Understand how the HoloDesktop CLI client, runtime, model backend, and host integrations fit together.
Think of HoloDesktop CLI as a thin client around a local computer-use agent runtime.
There are two main pieces:
* the open-source Python client, distributed as `holo-desktop-cli`;
* the computer-use agent runtime executable, `hai-agent-runtime`.
The client is the part you run from your terminal, MCP host, ACP host, or Python code. The runtime observes the screen, plans actions, clicks and types, and streams events back.
The same local runtime sits behind every surface. The main choice is where inference happens: H Company's hosted Models API or an OpenAI-compatible endpoint you provide.
## The same runtime behind every surface
The CLI, MCP server, ACP server, skill, and Python client all route work to the same local runtime. The surface changes how the agent is invoked. It does not create a separate kind of computer-use agent.
| Surface | Use it for |
| ------- | ----------------------------------------------------------------------------------- |
| CLI | Run one foreground desktop task from a terminal or script. |
| MCP | Let Claude Code, Cursor, Codex, or another MCP host call HoloDesktop CLI as a tool. |
| ACP | Let an ACP-compatible host delegate desktop work to HoloDesktop CLI as a sub-agent. |
| Skill | Give a host a reusable HoloDesktop CLI instruction surface. |
| Python | Start sessions and stream events from application code. |
For commands and flags, use [CLI reference](/holo-desktop-cli/reference/cli). For host setup, use the [Integrations](/holo-desktop-cli/integrations/use-from-cli) pages.
## Runtime lifecycle
The Python client starts or attaches to `hai-agent-runtime` on loopback. If a healthy runtime is already listening on the target port, the client reuses it. Otherwise, it starts one.
The runtime is local to your machine. It owns desktop observation and action. The client owns installation, launch, host integration, and user-facing commands.
For exact cache paths, logs, token files, and run directories, use [Paths and files](/holo-desktop-cli/reference/paths-and-files).
## Inference path
The runtime needs a model backend. HoloDesktop CLI has two modes:
* hosted mode, where the runtime sends task-relevant model inputs to H Company's Models API;
* local mode, where the runtime sends model inputs to the OpenAI-compatible endpoint you provide.
The rest of the architecture is the same in both modes: the runtime still runs locally, controls the desktop locally, and writes local diagnostics. The difference is where model inference happens.
For setup, use [Hosted or local models](/holo-desktop-cli/getting-started/hosted-or-local-models). For privacy implications, use [Security and privacy](/holo-desktop-cli/security-and-privacy).
## Desktop control
The agent operates desktop state, not source code or APIs. It observes what is visible, plans the next action, and uses desktop tools to click, type, scroll, and switch apps.
Foreground state matters. A CLI task can move focus and use the active desktop while it runs. Host integrations may make it feel like a tool call, but the underlying action is still desktop operation.
## User context
The client snapshots user context at run start and sends it to the runtime with the task. That context can include standing instructions, memories, rules, and installed skills from `~/.holo/`.
This makes the agent customizable, but those files can affect behavior. Keep persistent instructions specific and review them if a run behaves unexpectedly.
For the exact files, use [Paths and files](/holo-desktop-cli/reference/paths-and-files).
## Run artifacts
The runtime emits events as it works. Clients use those events to print progress, stream updates, and debug failures. The same stream is also persisted locally as run artifacts.
Those artifacts are diagnostics, not a product analytics upload. In standalone HoloDesktop CLI mode, run traces stay on the user's machine unless the user chooses to share them.
For event structure, use [Debug a failed run](/holo-desktop-cli/how-to/debug-failed-run). For storage and privacy, use [Paths and files](/holo-desktop-cli/reference/paths-and-files) and [Security and privacy](/holo-desktop-cli/security-and-privacy).
## Verification pattern
The agent can observe and act, but strong workflows should still verify outputs separately when possible.
Strong examples follow this pattern:
1. Stage known inputs.
2. Ask the agent to perform visible desktop work.
3. Persist run artifacts.
4. Verify the resulting files, UI state, or external state deterministically.
This keeps the roles clear: the agent performs the visible desktop work; the verifier decides whether the work met the contract. The [expense-report example](/holo-desktop-cli/examples/expense-report-automation) shows that pattern end to end.
# Find and fix a UI bug with Claude Code
Source: https://hub.hcompany.ai/holo-desktop-cli/examples/claude-code-ui-qa
Use HoloDesktop CLI through MCP from Claude Code to test a running app, fix the source, and verify the UI.
Claude Code can read and edit your project, but it cannot use a running app like a user. HoloDesktop CLI fills that gap: Claude Code delegates a desktop task to the CLI, the CLI operates the app, and Claude Code uses the UI evidence to make and verify a code change.
```mermaid theme={null}
flowchart LR
user["Prompt Claude Code"] --> claude["Claude Code"]
claude --> mcp["holo_desktop MCP tool"]
mcp --> nimbus["Nimbus Desk in browser"]
nimbus --> mcp
mcp --> report["QA report"]
report --> claude
claude --> patch["Patch source"]
patch --> verify["Ask HoloDesktop CLI to verify again"]
```
## What you'll do
In this example, you will:
* run the Nimbus Desk demo app;
* open Claude Code from the Nimbus workspace;
* ask Claude Code to make a small UI change;
* let Claude Code use HoloDesktop CLI to run the relevant behavioral QA spec;
* have Claude Code fix the bug that the CLI observes;
* verify the fix with HoloDesktop CLI.
This is a closed loop: observe the app, diagnose the source, patch the code, and verify the real UI again.
## The scenario
Nimbus Desk is a small support dashboard inside the HoloDesktop CLI checkout. It has a Tickets page with a `Status` dropdown. The behavioral spec says that selecting `Open` should show exactly three open tickets: `#2042`, `#2040`, and `#2036`.
The app contains a realistic status-filter bug. The source looks plausible, but the UI behavior is wrong: the filter compares each ticket's `status` object to the selected string value.
## Start Nimbus
From the `holo-desktop-cli` checkout:
```bash theme={null}
uv sync
cd examples/software_qa/nimbus-desk
npm install
npm run dev
```
The app should be available at:
```text theme={null}
http://localhost:5173
```
Demo credentials are shown on the login page:
```text theme={null}
demo@nimbus.test / holo-qa-1
```
## Prepare HoloDesktop CLI
In another terminal, sign in for hosted mode:
```bash theme={null}
cd /path/to/holo-desktop-cli
uv run holo login
```
Or use local mode by making sure the process that launches Claude Code can see:
```bash theme={null}
export HAI_AGENT_RUNTIME_BASE_URL=http://localhost:8000/v1
export HAI_AGENT_RUNTIME_MODEL=Hcompany/Holo-3.1-35B-A3B
```
Nimbus checks in a `.mcp.json` file that points Claude Code at the root HoloDesktop CLI MCP server:
```json theme={null}
{
"mcpServers": {
"holo": {
"command": "uv",
"args": ["run", "--directory", "../../..", "holo", "mcp"]
}
}
}
```
Open Claude Code from the Nimbus workspace so that workspace-local MCP config is active:
```bash theme={null}
cd /path/to/holo-desktop-cli/examples/software_qa/nimbus-desk
claude
```
## Give Claude Code the closed-loop prompt
Ask Claude Code:
```text theme={null}
Add a Priority dropdown beside the Status dropdown on the Tickets page, with
options All, High, Medium, and Low.
After the change, use the local HoloDesktop CLI QA workflow to run the relevant behavioral
spec for the Tickets page. If the CLI reports a failure, inspect the source, make
the minimal fix, and rerun the same spec once to verify the UI.
Do not stop after reporting the bug unless you cannot identify a safe fix.
```
This prompt asks for a normal product change first. Claude Code should edit the app, then use HoloDesktop CLI as a black-box tester. The CLI should catch the existing status-filter bug from the visible UI behavior.
## What should happen
Claude Code should follow this loop:
```mermaid theme={null}
flowchart TD
change["Implement Priority dropdown"] --> qa1["Run tickets-filter spec with HoloDesktop CLI"]
qa1 --> fail["HoloDesktop CLI reports VERDICT: FAILED"]
fail --> inspect["Claude Code inspects Tickets.jsx"]
inspect --> fix["Fix status filtering"]
fix --> qa2["Rerun tickets-filter spec with HoloDesktop CLI"]
qa2 --> pass["HoloDesktop CLI reports VERDICT: PASSED"]
```
The first QA run should fail because selecting `Open` does not show the expected open tickets. Claude Code should then inspect the Tickets page source and fix the filter logic.
HoloDesktop CLI provides UI evidence, and Claude Code uses that evidence to find the source-level bug.
## The fix Claude Code should discover
The relevant code lives in:
```text theme={null}
examples/software_qa/nimbus-desk/src/pages/Tickets.jsx
```
The broken logic compares an object to a string:
```jsx theme={null}
visible = TICKETS.filter((ticket) => ticket.status === statusFilter);
```
The fix is to compare the ticket's status key:
```jsx theme={null}
visible = TICKETS.filter((ticket) => ticket.status.key === statusFilter);
```
If Claude Code also adds the Priority dropdown, the final implementation should apply both filters. Do not change the QA spec just to make the test pass.
## Verify the fix
After the patch, Claude Code should ask HoloDesktop CLI to run the same `tickets-filter` spec again.
A good final result looks like:
```text theme={null}
VERDICT: PASSED
Observed:
- Status dropdown is set to Open.
- The table shows exactly #2042, #2040, and #2036.
- No Closed or Reopened tickets are visible.
- No error message appears.
```
## Run a passing QA check too
The ticket spec is useful because it catches a regression. HoloDesktop CLI is also useful when the flow already works. It can produce positive UI evidence that a real user path still behaves correctly.
Nimbus includes a passing chat spec:
```text theme={null}
examples/software_qa/nimbus-desk/qa/chat-widget.md
```
Ask Claude Code to run it after the ticket fix:
```text theme={null}
Now run the HoloDesktop CLI QA spec for the chat widget without changing source.
Treat this as a positive smoke check: sign in, open the chat, send the refund
question, wait for the assistant reply, and report the visible evidence.
```
The expected flow is small but realistic. Starting from a signed-out browser session, HoloDesktop CLI navigates to the app, signs in, lands on the dashboard, opens the chat widget, sends `How do refunds work?`, waits for the assistant response, and verifies that the reply mentions refunds being processed within 5 business days.
A passing run should read like evidence, not just a green checkbox:
```text theme={null}
VERDICT: PASSED
Observed:
- The dashboard loaded after sign-in.
- The Nimbus assistant opened from the chat bubble.
- The user message appeared in the chat log.
- The assistant replied with refund policy details.
- The reply offered the Talk to a human option.
```
This is the shape of a CI or release smoke test: keep the Markdown spec stable, run HoloDesktop CLI against the built app, and store the final report plus screenshots as artifacts. Unlike a unit test, the evidence is user-visible behavior.
## Why this works
This example works because each system has a clear job:
| System | Role |
| --------------- | ----------------------------------------------------------------------------------- |
| Claude Code | Reads instructions, edits source, reasons about the bug, applies the patch |
| HoloDesktop CLI | Opens the app, signs in, clicks the UI, observes visible behavior, reports evidence |
| QA spec | Defines the expected user-visible behavior in plain Markdown |
The second HoloDesktop CLI run matters. It prevents a code-only fix from being accepted just because the patch looks right.
## Troubleshooting
| Symptom | Try |
| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| Claude Code cannot find `holo_desktop` | Open Claude Code from `examples/software_qa/nimbus-desk` so the checked-in `.mcp.json` is active |
| HoloDesktop CLI asks for login | Run `uv run holo login` from the `holo-desktop-cli` checkout, or configure local model env vars before starting Claude Code |
| The app is unreachable | Confirm `npm run dev` is running and `http://localhost:5173` returns `200` |
| HoloDesktop CLI cannot observe or click | Check macOS Screen Recording and Accessibility permissions |
| HoloDesktop CLI reports a failure twice | Stop and inspect both reports; the second failure is usually a different bug or an incomplete fix |
## What to copy into your own project
For your own app, copy the pattern, not the Nimbus details:
1. Write a small Markdown QA spec for one user-visible behavior.
2. Configure Claude Code to call HoloDesktop CLI through MCP.
3. Ask Claude Code to make a product change.
4. Require Claude Code to verify the affected flow with HoloDesktop CLI.
5. If HoloDesktop CLI reports a failure, fix once and verify again.
Use this pattern for authenticated UI flows, visual regressions, settings screens, local web apps, and native apps where source code alone is not enough evidence.
# Automate an expense report from the CLI
Source: https://hub.hcompany.ai/holo-desktop-cli/examples/expense-report-automation
Run HoloDesktop CLI directly from the command line to read receipts, update a spreadsheet, draft an email, and verify the result.
This example shows the simplest direct HoloDesktop CLI workflow: run a desktop task from the command line, let the CLI operate normal apps, and verify the result with deterministic checks.
HoloDesktop CLI reads three staged receipt files, adds one expense row per receipt to a LibreOffice Calc ledger, saves the spreadsheet, and creates an unsent Mail draft with the sheet attached.
```mermaid theme={null}
flowchart LR
cli["holo-demo run expense_report"] --> stage["Stage pinned files"]
stage --> apps["Open Finder, LibreOffice, and Mail"]
apps --> holo["HoloDesktop CLI operates the desktop"]
holo --> ledger["Updated ledger"]
holo --> draft["Mail draft"]
ledger --> verify["Deterministic verifier"]
draft --> verify
verify --> artifacts["events.jsonl, task.json, summary CSV"]
```
## What this teaches
The Claude Code example shows HoloDesktop CLI inside an agent host. This example removes the host and shows the CLI as a standalone desktop operator:
* the CLI starts the run;
* the harness prepares the desktop state;
* HoloDesktop CLI observes receipts and controls apps visually;
* verifiers decide whether the run actually worked;
* run artifacts explain what happened step by step.
Use this pattern for local demos, eval harnesses, and release smoke checks that need real desktop behavior rather than only API calls.
## Before you run it
This is a full desktop automation run and may take several minutes. On a first run, macOS or the apps may show permission and onboarding dialogs.
During the run:
* do not use the machine for other work;
* expect Finder, LibreOffice, Preview or QuickLook, and Mail to come to the foreground;
* allow app permissions if macOS asks for access to Desktop files;
* do not click Send in Mail if you take over manually.
If you only want to inspect what the harness will do, use `--dry-run` first.
## Prerequisites
From the `holo-desktop-cli` checkout, install the workspace:
```bash theme={null}
uv sync --all-groups
```
The example currently targets macOS and expects:
* LibreOffice installed at `/Applications/LibreOffice.app`;
* Mail available locally;
* HoloDesktop CLI configured for hosted mode or local model mode;
* the managed `hai-agent-runtime` installed, or available on `PATH`.
If you have not run HoloDesktop CLI before, a first real run may download the pinned desktop runtime under:
```text theme={null}
~/.holo/runtime/
```
For hosted mode, sign in before running the demo:
```bash theme={null}
uv run holo login
```
For local mode, start your OpenAI-compatible model server and pass `--base-url` and `--model` to the demo command.
## Install the demo skill
The demo includes a small HoloDesktop CLI skill that gives the agent task-specific guidance for expense-report work. Install it once:
```bash theme={null}
cd examples/holo-demos
uv run holo-demo install-skills
```
The installer copies the bundled skill into:
```text theme={null}
~/.holo/skills/expense-report/
```
It is safe to rerun. If you edit the bundled skill later, pass `--force` to overwrite the installed copy.
## Pin the fixtures
The receipts and spreadsheet template come from OSWorld fixtures hosted on Hugging Face. The manifest pins every downloaded file by `sha256`, so the verifier checks known inputs rather than a drifting dataset.
```bash theme={null}
uv run holo-demo pin-fixtures manifests/expense_report.toml
```
The fixtures are written under:
```text theme={null}
examples/holo-demos/fixtures/expense_report/
```
The demo only stages the first three receipts to keep the run short.
## Dry-run first
Run a dry-run to see the app launch plan, focus target, prompt, and runtime options without letting HoloDesktop CLI control the desktop:
```bash theme={null}
uv run holo-demo run expense_report --dry-run
```
You should see output like:
```text theme={null}
[dry-run] expense_report: would launch ['com.apple.finder', 'org.libreoffice.script', 'com.apple.mail']
[dry-run] expense_report: would focus 'org.libreoffice.script'
[dry-run] expense_report: output would go to runs/expense_report
```
Use this to inspect the harness before running the actual task.
## Run the demo
Start the real run:
```bash theme={null}
uv run holo-demo run expense_report
```
For a hosted model, the run uses your HoloDesktop CLI login. For a local OpenAI-compatible server, pass the model connection explicitly:
```bash theme={null}
uv run holo-demo run expense_report \
--base-url http://localhost:8000/v1 \
--model Hcompany/Holo-3.1-35B-A3B
```
You can also adjust the cap:
```bash theme={null}
uv run holo-demo run expense_report --max-steps 60
```
## What HoloDesktop CLI does
Before HoloDesktop CLI starts acting, the harness copies files into predictable Desktop locations:
```text theme={null}
~/Desktop/holo-demo-receipts/
~/Desktop/holo-demo-bookkeeping.xlsx
```
It then opens Finder, LibreOffice Calc, and Mail, and focuses LibreOffice so the agent begins from the ledger.
The task asks HoloDesktop CLI to:
* inspect each receipt or invoice using QuickLook or Preview;
* identify what was bought and the grand total;
* append a row with Description, Category, Type, and Amount;
* record expenses as negative amounts;
* save the spreadsheet;
* create a new Mail draft to `expenses@example.com`;
* set the subject to `Expenses`;
* attach the saved spreadsheet;
* leave the message in Drafts.
On a normal run, the three staged receipts should produce rows like:
| Description | Category | Type | Amount |
| ----------- | -------- | ------- | ---------- |
| Grocery | Food | Expense | `-186.93` |
| Cash Out | Other | Expense | `-3670.00` |
| Soup | Food | Expense | `-5.70` |
## How verification works
The example does not rely on a human watching the desktop and deciding whether it looked right. After the run, deterministic verifiers inspect the artifacts.
The `ledger_rows` verifier opens the spreadsheet with `openpyxl` and checks:
* exactly one new row exists per staged receipt;
* the new amount values match the pinned receipt totals;
* amounts are compared sign-insensitively with a one-cent tolerance.
The `mail_draft` verifier uses AppleScript to check:
* there is a draft addressed to `expenses@example.com`;
* the subject is `Expenses`;
* the draft has at least one attachment.
A passing run should report a verification summary like:
```text theme={null}
verify_pass=2/2
```
If Mail automation permission is denied, the Mail check fails as a harness failure rather than as evidence that HoloDesktop CLI made the wrong UI decision.
## Run artifacts
Each run writes a timestamped directory under:
```text theme={null}
examples/holo-demos/runs/expense_report/
```
The important files are:
| File | What it is for |
| ------------------------ | --------------------------------------------------------------------------------------- |
| `events.jsonl` | One runtime event per line: observations, model reasoning, tool calls, and step timing. |
| `task.json` | The task prompt, result, status, verification results, and run metadata. |
| `runs/summary-demos.csv` | One summary row per demo run, including verification status. |
Interrupted runs are still useful. If you stop the run early, the summary shows it as interrupted and verification may be skipped or incomplete, but `events.jsonl` still records what HoloDesktop CLI saw and did.
After verification, staged Desktop files are moved into:
```text theme={null}
~/.holo/runs/expense_report-quarantine/
```
That keeps the touched files available for inspection without leaving the Desktop cluttered between runs.
## Troubleshooting
If the run gets distracted by another app, stop it and rerun from a quiet desktop session. HoloDesktop CLI controls the visible desktop, so foreground apps matter.
If LibreOffice shows a welcome or file-access dialog, allow it and let the run continue. These first-run dialogs are normal on a fresh machine.
If Mail shows a privacy or onboarding dialog, complete the prompt before expecting the Mail verifier to pass.
If fixture download fails, rerun:
```bash theme={null}
uv run holo-demo pin-fixtures manifests/expense_report.toml
```
If the verifier reports a spreadsheet mismatch, inspect the quarantined workbook and compare it with `events.jsonl` to see whether HoloDesktop CLI read the receipt incorrectly, typed into the wrong cell, or was interrupted before saving.
## Adapt the pattern
The reusable pattern is:
1. Stage known inputs in predictable locations.
2. Launch the apps the task needs.
3. Focus the app where HoloDesktop CLI should begin.
4. Give HoloDesktop CLI a concrete desktop task.
5. Verify the output with code.
6. Keep the run artifacts for debugging and evaluation.
That pattern works well for demos and evals because it separates visible desktop behavior from pass/fail logic. HoloDesktop CLI does the user-visible work; deterministic verifiers decide whether the work met the contract.
# Examples
Source: https://hub.hcompany.ai/holo-desktop-cli/examples/index
Learn HoloDesktop CLI through complete workflows.
These examples pick up after installation. They show HoloDesktop CLI doing real desktop work, observing the result, and using that evidence to decide what to do next.
## Start here
Use HoloDesktop CLI through MCP so Claude Code can test a running app, inspect a failure, patch the source, and verify the fix.
Run HoloDesktop CLI directly from the command line to read receipts, update a spreadsheet, draft an email, and verify the result.
# Hosted or local models
Source: https://hub.hcompany.ai/holo-desktop-cli/getting-started/hosted-or-local-models
Choose how HoloDesktop CLI calls a model backend for desktop tasks.
Before HoloDesktop CLI can run a task, it needs a model. You have two choices: H Company's hosted Models API, or a model you run yourself and point the CLI at over an OpenAI-compatible server.
Hosted mode is the default. Local mode kicks in the moment you provide a local base URL.
## Hosted mode
Sign in once:
```bash theme={null}
holo login
```
This opens the H Company Portal in your browser. After sign-in, the CLI writes a hosted API key to `~/.holo/.env`. Check that the key is available:
```bash theme={null}
holo whoami
```
You can also provide `HAI_API_KEY` through the process environment. When the key comes from the environment, `holo whoami` may not have a cached Portal identity to print.
To pick a hosted model explicitly, pass its API model ID:
```bash theme={null}
holo run --model holo3-1-35b-a3b "Open TextEdit and write a short note saying HoloDesktop CLI is installed"
```
For the larger Holo3 hosted model, use `--model holo3-122b-a10b`.
MCP and ACP hosts cannot complete browser login during startup, because they launch the CLI non-interactively. If you plan to use hosted mode from a host, run `holo login` in a terminal first, then restart the host so it can see the saved key.
## Local mode
Local mode runs one of H Company's open-weight models on your own machine, for fully private, on-device inference. You bring up an OpenAI-compatible server, then point the CLI at it.
To set up the server itself, follow [Run a local model server](/holo-desktop-cli/how-to/run-a-local-model-server). It covers llama.cpp on macOS and vLLM on DGX Spark, including tuned launch flags. Open-weight weights live in the [H Company Hugging Face org](https://huggingface.co/Hcompany).
With a server running, point the CLI at it with two flags:
* `--base-url`: the address of your local server, such as `http://localhost:8080/v1` (llama.cpp) or `http://localhost:8000/v1` (vLLM).
* `--model`: the model ID. Any string works for llama.cpp; for vLLM it must match the `--served-model-name` you set when launching the server.
```bash theme={null}
holo run "Open TextEdit and write hello" \
--base-url http://localhost:8080/v1 \
--model holo3-1-35b
```
Local mode is selected the moment you provide `--base-url` or `HAI_AGENT_RUNTIME_BASE_URL`, and does not require `holo login` when the endpoint is reachable.
### Local mode from hosts
MCP and ACP hosts start the CLI over stdio, so they read model settings from the environment that launched the host. Set the local server URL before the host starts the CLI:
```bash theme={null}
export HAI_AGENT_RUNTIME_BASE_URL=http://localhost:8000/v1
```
If your server needs a model ID, set it too:
```bash theme={null}
export HAI_AGENT_RUNTIME_MODEL=Hcompany/Holo-3.1-35B-A3B
```
When `HAI_AGENT_RUNTIME_BASE_URL` is set, MCP and ACP startup does not require `HAI_API_KEY`. Shell exports usually do not reach GUI apps launched from the Dock or Finder, so if local mode works in your terminal but fails in a host, put `HAI_AGENT_RUNTIME_BASE_URL` and `HAI_AGENT_RUNTIME_MODEL` in the host's own MCP or ACP environment config.
## Which should I use?
Use hosted mode for the fastest setup. Use local mode for private inference, local model serving, or direct control over the model runtime.
## What's next
After hosted login succeeds or your local server is running, run your first task with the [Quickstart](/holo-desktop-cli/getting-started/quickstart).
# Quickstart
Source: https://hub.hcompany.ai/holo-desktop-cli/getting-started/quickstart
Install HoloDesktop CLI, connect a model, and run your first desktop task in minutes.
You'll have HoloDesktop CLI running a real task in a few minutes. On macOS, Windows, and Linux, the installer sets up the `holo` command, a private Python toolchain, and the managed desktop runtime for you. You need an H Company account for hosted mode.
Install the `holo` command, then verify it.
```bash theme={null}
curl -fsSL https://install.hcompany.ai/install.sh | bash
```
```powershell theme={null}
irm https://install.hcompany.ai/install.ps1 | iex
```
```bash theme={null}
curl -fsSL https://install.hcompany.ai/install.sh | bash
```
Managed runtime downloads support macOS on Apple Silicon, Linux on x86\_64, and Windows on x86\_64 or ARM64. The Windows installer detects the architecture automatically.
Open a new terminal after install, then check the command:
```bash theme={null}
holo --help
```
You should see subcommands such as `run`, `mcp`, `acp`, `install`, `login`, `whoami`, `doctor`, and `serve`. If `holo` is not found, reopen your shell so the freshly installed command is on `PATH`.
Hosted mode is the default. Sign in once, then confirm the key is available:
```bash theme={null}
holo login
holo whoami
```
To run a model yourself instead, bring up a local OpenAI-compatible server and pass `--base-url`. See [Run a local model server](/holo-desktop-cli/how-to/run-a-local-model-server) and [Hosted or local models](/holo-desktop-cli/getting-started/hosted-or-local-models).
```bash theme={null}
holo run "Open TextEdit and write a short note saying HoloDesktop CLI is installed"
```
Add `--model holo3-1-35b-a3b` to override the default hosted model.
```bash theme={null}
holo run \
--base-url http://localhost:8000/v1 \
"Open TextEdit and write a short note saying HoloDesktop CLI is installed"
```
Add `--model Hcompany/Holo-3.1-35B-A3B` if your server requires a model ID.
The first run downloads the agent runtime for you (sha256-verified, into `~/.holo/runtime/`). On macOS, it may prompt for Screen Recording and Accessibility; grant both and retry. See [Desktop permissions](#desktop-permissions) below for the per-platform requirements. The CLI drives one screen, the primary display, so keep the target app there and mirror or disconnect extra displays for important runs.
HoloDesktop CLI takes control of the visible desktop while a task runs. It can open apps, switch focus, click, type, and read whatever is on screen until the task finishes, times out, or is cancelled.
To stop a run, press `Esc` twice quickly: a global kill switch that works even while Holo holds focus and you cannot tab back to the terminal. You can also press `Ctrl+C` in the launching terminal, run `holo stop` from any terminal, or cancel from your MCP or ACP host. Bound risky tasks with `--max-steps` or `--max-time-s`.
The task prints progress and a final answer in your terminal. It worked if TextEdit opens and contains the requested note.
To stop a run, press `Esc` twice quickly. This is a global kill switch, so it works even while Holo has focus and you cannot tab back to the terminal. You can also press `Ctrl+C` in the launching terminal, or run `holo stop` from any other terminal. Bound longer tasks with `--max-steps` or `--max-time-s`.
Per-run event logs are written under `~/.holo/runs/`. If the run failed or the output was unclear, see [Debug a failed run](/holo-desktop-cli/how-to/debug-failed-run).
## Desktop permissions
To observe and control the screen, the runtime needs OS-level permission on some platforms. Grant these to the app that launches the CLI, usually your terminal, or your MCP or ACP host.
Under System Settings → Privacy & Security, grant the launching app:
* **Screen Recording**, so the runtime can see the desktop;
* **Accessibility**, so it can click and type;
* **Input Monitoring**, so the double-Esc kill switch can stop a run. Without it, use `holo stop` instead.
Restart the launching app after granting a permission. Grants do not apply to an already-running process. The first run prompts for these automatically.
No extra permission is required. Some elevated apps only accept input from an elevated process, so to drive them, start your terminal "as administrator".
Requires an X11 session; Wayland is not supported by the input backend. The first run downloads the managed Linux x86\_64 runtime automatically.
## Develop from source
Use this path only if you are changing HoloDesktop CLI itself. You need [Git](https://git-scm.com/), Python 3.12 or newer, and [`uv`](https://docs.astral.sh/uv/).
```bash theme={null}
git clone https://github.com/hcompai/holo-desktop-cli
cd holo-desktop-cli
make setup
uv run holo --help
```
From a source checkout, prefix CLI examples with `uv run`, such as `uv run holo doctor`.
***
## Next steps
Call HoloDesktop CLI from the terminal, an MCP or ACP host, or as a skill.
Complete workflows: a Claude Code UI-bug fix and a CLI expense report.
How the client, runtime, model backend, and host surfaces fit together.
Use doctor, logs, permissions, and model checks to localize a failure.
# Customize with skills, memories, and rules
Source: https://hub.hcompany.ai/holo-desktop-cli/how-to/customize
Shape how HoloDesktop CLI behaves across every run with optional files in ~/.holo.
HoloDesktop CLI reads a few optional files from `~/.holo/` at the start of every run and folds them into the agent's instructions. Use them to carry context across runs without repeating it in each task.
The snapshot is taken once when a run starts and frozen for that run, so edits apply to the next run, not the one in flight.
## Standing instructions
Put durable, always-on guidance in `~/.holo/agents.md`. It is prepended to every run.
```text ~/.holo/agents.md theme={null}
Prefer keyboard shortcuts over mouse clicks.
When a task is ambiguous, choose the least destructive option and report what you did.
```
An optional `name` in frontmatter personalizes the opening line:
```text ~/.holo/agents.md theme={null}
---
name: Antoine
---
Default to dark mode when an app offers it.
```
## Memories and rules
`memories.md` and `rules.md` are lists. Separate entries with a blank line; a file with no blank lines is read one entry per line.
```text ~/.holo/memories.md theme={null}
The expense portal is at https://expenses.internal.example.com.
My default browser is Firefox.
```
```text ~/.holo/rules.md theme={null}
Never submit a form without showing me the final values first.
Stop and ask before deleting files.
```
Memories are context the agent may use; rules are constraints it should follow. Both are loaded into every run alongside `agents.md`. HoloDesktop CLI also reads its own `holo-memories.md` in the same way, so leave that file to the CLI and keep your notes in `memories.md`. Each file is capped at 128 KB and content past the cap is truncated with a marker, so keep entries short and specific.
## Skills
A skill is a reusable, named procedure the agent can pull in when a task matches its description. Each lives in its own directory:
```text theme={null}
~/.holo/skills//SKILL.md
```
The directory name becomes the skill name, so keep it lowercase and hyphenated. `SKILL.md` needs YAML frontmatter with a `description` (used for matching, 280 characters max) and a markdown body with the steps:
```text ~/.holo/skills/file-an-expense/SKILL.md theme={null}
---
description: File an expense report in the internal portal from a receipt image.
---
1. Open the expense portal in the browser.
2. Click "New expense" and upload the receipt.
3. Fill amount, date, and category from the receipt.
4. Show the filled form and stop before submitting.
```
HoloDesktop CLI seeds its bundled skills into `~/.holo/skills/` on the first run and tracks them in `~/.holo/settings.json`, so your own skills sit alongside them. A skill that is missing its description or body is skipped with a warning.
To package a skill so an agent host can install it, see [Use HoloDesktop CLI as a skill](/holo-desktop-cli/integrations/use-as-skill).
## Verify what loaded
`holo doctor` reports how many skills are seeded under `~/.holo/`:
```bash theme={null}
holo doctor
```
For the full list of customization files and other local state, see [Paths and files](/holo-desktop-cli/reference/paths-and-files).
# Debug a failed run
Source: https://hub.hcompany.ai/holo-desktop-cli/how-to/debug-failed-run
Use doctor, logs, permissions, and model checks to debug a HoloDesktop CLI run.
Use this guide when `holo run`, `holo mcp`, or `holo acp` starts but does not finish the desktop task.
## Quick checklist
Run through these first. Most first-run failures are one of them.
| Check | Command or location | Success looks like |
| ----------------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| CLI is available | `holo --help` | Help prints without an import or command error |
| Hosted mode is ready | `holo whoami` | Your signed-in identity prints |
| Local mode is ready | `holo run --base-url http://localhost:8000/v1 ...` | The CLI reaches your local server and does not require login |
| Runtime is available | `holo doctor` | The `binary` row resolves a runtime path |
| Logs are written | `~/.holo/runs/` and `~/.holo/logs/` | Per-run and runtime logs appear after a task |
| Desktop permissions granted | [Desktop permissions](/holo-desktop-cli/getting-started/quickstart#desktop-permissions) | The CLI can observe the screen and click and type |
| Stop works | `Esc` twice, `Ctrl+C`, `holo stop`, or cancel from the host | The active run stops or reports interrupted |
| App is on the primary display | Display settings | The CLI can see and control the app it needs |
## Start with doctor
```bash theme={null}
holo doctor
```
`holo doctor` is read-only. It checks the runtime binary, hosted or local model credentials, the agent API port, and the `~/.holo` directory. Fix the first failing check before moving on.
## Reproduce with the CLI
If the failure happened from MCP or ACP, reproduce the smallest version from a terminal first:
```bash theme={null}
holo run \
--max-steps 5 \
"Open TextEdit and write the word test"
```
For local mode, include the same base URL and model that the host uses:
```bash theme={null}
holo run \
--base-url http://localhost:8000/v1 \
--model Hcompany/Holo-3.1-35B-A3B \
--max-steps 5 \
"Open TextEdit and write the word test"
```
If the CLI works but the host does not, check host configuration, environment inheritance, and workspace scope.
## Stop a bad run first
If HoloDesktop CLI is acting in the wrong app, typing in the wrong place, or looping, cancel the active run before debugging. The fastest stop is pressing `Esc` twice quickly, a global kill switch that works even while Holo holds focus. You can also press `Ctrl+C` in the launching terminal, or run `holo stop` from any terminal (`holo stop --force` also kills the runtime). For MCP or ACP runs, run `holo stop` or cancel the request from the host. To bound runs ahead of time, pass `--max-steps` or `--max-time-s`.
## Check model mode
For hosted mode:
```bash theme={null}
holo whoami
```
If this fails, run:
```bash theme={null}
holo login
```
For local mode, confirm the OpenAI-compatible endpoint is reachable:
```bash theme={null}
curl http://localhost:8000/v1/models
```
Then retry HoloDesktop CLI with `--base-url`. Local mode does not require `holo login`.
## Check host environment
MCP and ACP hosts launch HoloDesktop CLI non-interactively. Hosted mode needs `HAI_API_KEY` available to that process, usually from `~/.holo/.env` after `holo login`.
Local mode needs:
```bash theme={null}
HAI_AGENT_RUNTIME_BASE_URL=http://localhost:8000/v1
```
and sometimes:
```bash theme={null}
HAI_AGENT_RUNTIME_MODEL=Hcompany/Holo-3.1-35B-A3B
```
Terminal-launched hosts inherit shell exports. GUI apps launched from the Dock or Finder usually need these values in the host's MCP or ACP environment settings.
For Claude Code, also confirm you installed HoloDesktop CLI from the workspace where you are using Claude Code:
```bash theme={null}
cd /path/to/your/claude-code-workspace
holo install claude-code
```
Claude Code's MCP CLI uses local scope by default, so the MCP server registration is associated with the workspace where the command ran.
## Check runtime resolution
HoloDesktop CLI resolves `hai-agent-runtime` in this order:
1. `hai-agent-runtime` on `PATH`
2. managed install under `~/.holo/runtime/`
3. download-on-first-run
Run:
```bash theme={null}
holo doctor
```
The `binary` row shows which runtime path HoloDesktop CLI will use. If you are testing a local runtime build, put a deliberate `hai-agent-runtime` wrapper on `PATH`. Otherwise, let the CLI use the managed runtime.
## Check logs
Runtime startup logs live under:
```text theme={null}
~/.holo/logs/
```
Per-run event logs live under:
```text theme={null}
~/.holo/runs/
```
To isolate one run, choose a temporary run directory:
```bash theme={null}
holo run \
--runs-dir /tmp/holo-runs \
--max-steps 5 \
"Open TextEdit and write the word test"
```
For timing output, add:
```bash theme={null}
--profile
```
Use runtime logs for startup, model, port, permission, and binary-resolution failures. Use run logs when the runtime started but the task failed, timed out, or acted in the wrong place.
## Understand run logs
Each run writes a JSONL event log under:
```text theme={null}
~/.holo/runs//events.jsonl
```
For a normal `holo run`, the run directory usually contains `events.jsonl`. Observation events inside that file can include the raw screenshot HoloDesktop CLI saw as a base64 JPEG, plus metadata such as viewport size, cursor position, and sometimes accessibility data. Treat run logs as sensitive: they may contain screenshots, task text, app content, and model reasoning.
Do not post `events.jsonl`, `~/.holo/runs/`, or screenshots from a run publicly without review. They can include what the model saw on screen, including account names, local paths, messages, documents, and secrets.
Each line is one timestamped runtime event. The exact JSON shape is diagnostic and may change, so do not build production integrations against it. Use the sequence to find where a failed run got stuck.
| Event kind | What it means | What to look for |
| --------------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `message_event` | The task or follow-up message reached the runtime | Confirms the runtime received the task string you expected |
| `observation_event` | The runtime captured or processed the visible desktop state | If this is missing or followed by permission-looking errors, check Screen Recording |
| `policy_event` | The model planned the next action | Look for the planned tool name and whether the action matches the task |
| `tool_result_event` | A desktop action finished or failed | Look for failed clicks, typing, app activation, or other tool errors |
| `permission_request` | The runtime needs user approval for an app or capability | Grant or deny in the UI, then retry if the runtime needs to restart |
| `permission_decision` | A permission request was granted or denied | Denied permissions explain missing screenshots, clicks, or typing |
| `error_event` | The runtime surfaced an error during the session | Read the error text, then check `~/.holo/logs/` for startup details |
| `answer_event` | The runtime produced the final response | Confirms the run reached an end-of-turn rather than timing out |
Example event lines:
```json theme={null}
{"ts":"2026-06-15T12:00:00Z","event":{"kind":"message_event","text_content":"Open TextEdit and write test"}}
{"ts":"2026-06-15T12:00:01Z","event":{"kind":"observation_event","observation":{"kind":"computer","image":{"type":"base64","media_type":"image/jpeg","source":"..."}}}}
{"ts":"2026-06-15T12:00:03Z","event":{"kind":"policy_event","tool_reqs":[{"tool_name":"click_desktop"}]}}
{"ts":"2026-06-15T12:00:05Z","event":{"kind":"tool_result_event","tool_req":{"tool_name":"click_desktop"}}}
{"ts":"2026-06-15T12:00:06Z","event":{"kind":"answer_event","answer":"done"}}
```
Here is a shortened real log sketch for a run whose prompt was `Open Calculator and compute 2+2`:
```text theme={null}
message_event
content: Open Calculator and compute 2+2
observation_event
The runtime saw the desktop. Viewport: 1920x1243. Screenshot: embedded JPEG.
policy_event
note: open Calculator with Spotlight
tool: hotkey_desktop {"keys": ["cmd", "space"]}
tool_result
hotkey_desktop completed
policy_event
note: type Calculator into Spotlight
tool: write_desktop {"content": "Calculator", "overwrite": true}
tool_result
write_desktop completed
policy_event
note: press Enter to open Calculator
tool: hotkey_desktop {"keys": ["enter"]}
tool_result
hotkey_desktop completed
observation_event
Calculator is visible.
policy_event
note: click the plus button
tool: click_desktop {"element": "Orange plus (+) button"}
tool_result
click_desktop completed
policy_event
note: click the 2 button
tool: click_desktop {"element": "Number 2 button"}
tool_result
click_desktop completed
observation_event
Calculator shows 2+2.
answer_event
Final answer says the calculator shows 2+2 and clicking equals would display 4.
```
This run reached `answer_event`, but the event sequence shows the runtime never clicked `=`. That distinction matters: the runtime completed from the agent's perspective, but the task was not fully done.
Use the sequence to localize the failure:
| Log pattern | Usually means |
| ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| No `message_event` | The client did not create a session or wrote logs somewhere else |
| `message_event` but no `observation_event` | Runtime startup, screen capture, or permission problem |
| `observation_event` but no `policy_event` | Model backend did not respond, model request failed, or model was too slow |
| `policy_event` but no `tool_result_event` | Desktop tool execution failed or hung |
| Repeated `policy_event` / `tool_result_event` pairs | The agent is acting, but may be looping or stuck on the UI |
| `permission_request` followed by denied `permission_decision` | The run needs user approval before it can continue |
| `error_event` appears | The session failed inside the runtime; read the error and runtime log together |
| `answer_event` is present but task result is wrong | The run completed from the agent's perspective; improve the task string or inspect earlier actions |
For timing summaries, prefer:
```bash theme={null}
holo run --profile "Open TextEdit and write the word test"
```
`--profile` reads the event log and prints per-step observe, model, tool, and total timings.
## Check desktop permissions
A runtime that starts but cannot observe the screen or click is almost always a permissions problem. Grant permissions to the process that launches HoloDesktop CLI: for CLI runs that is usually your terminal; for MCP or ACP it may be the host app.
The requirements are per-platform (macOS needs Screen Recording and Accessibility; Windows needs none; Linux needs an X11 session). See [Desktop permissions](/holo-desktop-cli/getting-started/quickstart#desktop-permissions) for the full matrix. After granting on macOS, restart the launching process; grants do not apply to an already-running runtime.
## Common fixes
| Symptom | Likely cause | Try |
| ---------------------------------------------- | ------------------------------------ | -------------------------------------------------------------------------------------- |
| `No HAI_API_KEY found` | Hosted mode is not signed in | `holo login` |
| Local endpoint works in terminal but not host | Host did not inherit env vars | Add env vars to host config or launch host from that shell |
| Claude Code cannot see HoloDesktop CLI | Installed in a different workspace | Re-run `holo install claude-code` from the active workspace |
| Runtime starts but cannot observe | Screen Recording missing | Grant permission and restart the runtime |
| Runtime observes but cannot click/type | Accessibility missing | Grant permission and restart the runtime |
| A host-launched run will not stop | Host did not propagate cancellation | Cancel from the host, or close the host's stdio connection to HoloDesktop CLI |
| HoloDesktop CLI controls the wrong monitor | Target app is on a secondary display | Move the app to the primary display, mirror displays, or disconnect secondary monitors |
| `--base-url` run fails before contacting model | Runtime/config mismatch | Update to the fixed runtime/client version and retry |
| Task times out | Task is too broad or model is slow | Use a smaller task, `--max-steps`, or a faster model |
## What's next
Use [MCP](/holo-desktop-cli/integrations/use-mcp) if the failure only happens inside an agent host.
# Embed with Python
Source: https://hub.hcompany.ai/holo-desktop-cli/how-to/embed-with-python
Use HoloDesktop CLI's Python agent client to run desktop sessions from your own code.
Reach for the Python client when you want desktop sessions inside your own code, a script, demo harness, test runner, or local tool, instead of shelling out to `holo run`.
The Python client starts or attaches to the same local `hai-agent-runtime` process used by the CLI, MCP, and ACP surfaces.
## Basic script
After installing HoloDesktop CLI, import the agent client from `holo_desktop`:
```python theme={null}
import asyncio
from holo_desktop.agent_client import AgentApiClient, SpawnConfig, ensure_running
from holo_desktop.agent_client.requests import build_session_request
async def main() -> None:
daemon = await ensure_running(SpawnConfig(port=18795))
try:
async with AgentApiClient(daemon.base_url, daemon.token) as client:
request = build_session_request(
task="Open TextEdit and write a short note saying HoloDesktop CLI is embedded",
max_steps=10,
max_time_s=120,
)
session_id = await client.create_session(request)
stream = client.stream(session_id)
async for event in stream.events():
print(event.type)
print(stream.answer)
finally:
await daemon.aclose()
asyncio.run(main())
```
Run it with:
```bash theme={null}
python your_script.py
```
From a source checkout, run it through `uv` instead: `uv run python your_script.py`.
## Use hosted or local mode
Hosted mode uses the same login state as the CLI. Sign in once:
```bash theme={null}
holo login
```
Local mode passes model settings at runtime spawn:
```python theme={null}
daemon = await ensure_running(
SpawnConfig(
port=18795,
model="Hcompany/Holo-3.1-35B-A3B",
base_url="http://localhost:8000/v1",
)
)
```
The model and base URL are process-level settings. Start a separate runtime process when you need to switch model backend.
## Keep logs for a run
To choose where runtime event logs are written:
```python theme={null}
from pathlib import Path
daemon = await ensure_running(
SpawnConfig(
port=18795,
runs_dir=Path("/tmp/holo-runs"),
)
)
```
The CLI equivalent is:
```bash theme={null}
holo run --runs-dir /tmp/holo-runs "Open TextEdit and write test"
```
## Send another message
The client exposes pause, resume, cancel, and mid-run messages:
```python theme={null}
await client.pause(session_id)
await client.resume(session_id)
await client.send_message(session_id, "Continue, but do not send anything.")
await client.cancel(session_id)
```
Use these methods for interactive tools. For one-shot scripts, create a session, stream to end-of-turn, and close the daemon.
## Handle cleanup
Always close the daemon:
```python theme={null}
finally:
await daemon.aclose()
```
If your script spawned the runtime, closing the daemon stops it. If it attached to an already-running runtime on the same port, closing the daemon only releases the client-side handle. If your program is interrupted while a session is active, cancel the session or close the daemon before exiting, or the runtime may keep working until its timeout or safety budget.
## What to avoid
* Do not pass secrets or hidden context only in your app state. HoloDesktop CLI sees the task string and the configured `~/.holo` context, not your surrounding Python variables.
* Do not switch `model` or `base_url` per session. Those are runtime process settings.
* Do not leave a session running after your program exits. Cancel or close when interrupted.
## What's next
Use [Debug a failed run](/holo-desktop-cli/how-to/debug-failed-run) to inspect runtime logs, or see the [expense-report example](/holo-desktop-cli/examples/expense-report-automation) for a larger tested harness.
# Run a local model server
Source: https://hub.hcompany.ai/holo-desktop-cli/how-to/run-a-local-model-server
Serve an H Company open-weight model locally with llama.cpp or vLLM, then connect HoloDesktop CLI.
Local mode needs an OpenAI-compatible model server on your own machine. This guide covers llama.cpp on macOS and vLLM on DGX Spark, including the launch flags that matter. Open-weight weights live in the [H Company Hugging Face org](https://huggingface.co/Hcompany).
Holo3 122B is hosted API-only. Use `holo3-122b-a10b` in hosted mode rather than downloading local weights.
Good starting model IDs for local servers:
| Model | Example model ID |
| ------------------ | --------------------------------- |
| Holo 3.1 35B | `Hcompany/Holo-3.1-35B-A3B` |
| Holo 3.1 35B GGUF | `Hcompany/Holo-3.1-35B-A3B-GGUF` |
| Holo 3.1 35B NVFP4 | `Hcompany/Holo-3.1-35B-A3B-NVFP4` |
For native performance on macOS with Metal GPU acceleration, use llama.cpp with the open-weight `Q4_K_M` GGUF weights, which balance precision and speed. A MacBook Pro or Max with an M3 chip or newer and at least 36 GB of unified memory is recommended: the `Q4_K_M` weights use roughly 21 GB, and prefix caching improves performance but needs extra memory to pre-allocate the KV cache. Lower `--cache-ram` and `--ctx-size` to reduce memory use.
Install llama.cpp:
```bash theme={null}
brew install llama.cpp
```
Start the server:
```bash theme={null}
llama-server -hf Hcompany/Holo-3.1-35B-A3B-GGUF
```
For better efficiency, apply these tuned parameters:
```bash theme={null}
llama-server \
--hf Hcompany/Holo-3.1-35B-A3B-GGUF \
--n-gpu-layers 999 \
--ctx-size 65536 \
--batch-size 16384 \
--ubatch-size 2048 \
--flash-attn 1 \
--cache-type-k q8_0 \
--cache-type-v q8_0 \
--image-min-tokens 1024 \
--ctx-checkpoints 8 \
--cache-ram 32768 \
--kv-unified \
--threads 16
```
llama.cpp serves on port `8080` by default, so the base URL is `http://localhost:8080/v1`. Any string works as the `--model` value.
On DGX Spark, use the latest stable vLLM image for aarch64 (`v0.23.0`): `vllm/vllm-openai:v0.23.0-aarch64-cu129-ubuntu2404`. The open-weight NVFP4 weights use the Blackwell architecture's NVFP4 support for a good speed and precision tradeoff on GB10 GPUs.
Pull the image:
```bash theme={null}
docker pull vllm/vllm-openai:v0.23.0-aarch64-cu129-ubuntu2404
```
Launch the server:
```bash theme={null}
docker run -d --gpus all \
--shm-size=16g \
--network host \
-v ~/.cache/huggingface:/root/.cache/huggingface \
vllm/vllm-openai:v0.23.0-aarch64-cu129-ubuntu2404 \
vllm serve Hcompany/Holo-3.1-35B-A3B-NVFP4 \
--served-model-name holo3-1-35b \
--host 0.0.0.0
```
For better efficiency, apply these tuning parameters:
```bash theme={null}
docker run -d --gpus all \
--shm-size=16g \
--network host \
-v ~/.cache/huggingface:/root/.cache/huggingface \
vllm/vllm-openai:v0.23.0-aarch64-cu129-ubuntu2404 \
vllm serve Hcompany/Holo-3.1-35B-A3B-NVFP4 \
--served-model-name holo3-1-35b \
--host 0.0.0.0 \
--gpu-memory-utilization 0.8 \
--max-model-len 65537 \
--max-num-batched-tokens 32768 \
--chat-template-content-format openai \
--enable-auto-tool-choice \
--tool-call-parser qwen3_coder \
--reasoning-parser qwen3 \
--limit-mm-per-prompt '{"image": 5, "video": 0}' \
--mm-encoder-tp-mode data \
--mm-processor-cache-type shm \
--mm-processor-cache-gb 15
```
vLLM serves on port `8000` by default, so the base URL is `http://localhost:8000/v1`. The `--served-model-name` you set (`holo3-1-35b`) is the model ID you must pass to HoloDesktop CLI.
## Connect HoloDesktop CLI
With your server running, open a new terminal and point HoloDesktop CLI at it:
```bash theme={null}
holo run "Open TextEdit and write a short note saying HoloDesktop CLI is installed" \
--base-url http://localhost:8080/v1 \
--model holo3-1-35b
```
```bash theme={null}
holo run "Open TextEdit and write a short note saying HoloDesktop CLI is installed" \
--base-url http://localhost:8000/v1 \
--model holo3-1-35b
```
Local mode does not require `holo login`.
## What's next
Wire local mode into hosts with the environment variables in [Hosted or local models](/holo-desktop-cli/getting-started/hosted-or-local-models#local-mode-from-hosts), then run your first task with the [Quickstart](/holo-desktop-cli/getting-started/quickstart).
# HoloDesktop CLI
Source: https://hub.hcompany.ai/holo-desktop-cli/index
Use HoloDesktop CLI from your terminal, agent host, or Python code to delegate desktop work.
HoloDesktop CLI puts H Company's computer-use agent in your terminal. Describe a task in plain language and it looks at your screen, opens apps, clicks, and types to get it done. The agent runs entirely on your machine: the CLI starts the `hai-agent-runtime` binary locally and talks to it over loopback, so the desktop it drives is your own.
HoloDesktop CLI takes control of the visible desktop while a task runs. It can open apps, switch focus, click, type, and read whatever is on screen until the task finishes, times out, or is cancelled.
To stop a run, press `Esc` twice quickly: a global kill switch that works even while Holo holds focus and you cannot tab back to the terminal. You can also press `Ctrl+C` in the launching terminal, run `holo stop` from any terminal, or cancel from your MCP or ACP host. Bound risky tasks with `--max-steps` or `--max-time-s`.
## Start here
Install, connect a model, and run your first task in a few minutes.
Drive the CLI from the terminal, an MCP or ACP host, or as a skill.
Use an H Company hosted model, or run one yourself for private, on-device inference.
Doctor, logs, permissions, and model checks when a run goes sideways.
## What you can do
**Automate a desktop chore.** Hand it a one-shot task from your terminal and watch it work:
```bash theme={null}
holo run "Open the Calculator and compute 12 percent of 4,800"
```
**Give your coding agent eyes and hands.** Connect the CLI to Claude Code, Cursor, or any MCP or ACP host so the agent can drive a real app, not just read files. A coding agent can fix a bug, then ask the CLI to click through the live UI and confirm it.
**Build it into your own tools.** Drive desktop sessions from Python to script demos, tests, or internal automations.
Each of these starts the same local runtime. The way you launch it changes; the agent underneath still observes the screen, plans, clicks, types, and streams events back to you.
## Supported platforms
The `holo` client is pure Python and runs anywhere Python does. What varies is the `hai-agent-runtime` binary it drives, which is published for some platforms and bring-your-own for the rest.
| Platform | Agent runtime | Notes |
| -------------------- | ----------------------- | -------------------------------------------------------- |
| macOS, Apple Silicon | Downloaded on first run | Needs Screen Recording and Accessibility |
| Windows, x86\_64 | Downloaded on first run | No extra permissions |
| Windows, ARM64 | Downloaded on first run | No extra permissions |
| macOS, Intel | Bring your own | Put `hai-agent-runtime` on `PATH`, or set a download URL |
| Linux, x86\_64 | Downloaded on first run | Needs an X11 session; Wayland is not supported |
The CLI resolves the runtime in order: `hai-agent-runtime` on `PATH`, then a managed install under `~/.holo/runtime/`, then a sha256-verified download on first run. On bring-your-own platforms, install the `holo` client normally, then make a `hai-agent-runtime` executable resolvable on `PATH` (or point `HAI_AGENT_RUNTIME_DOWNLOAD_URL`, with its matching `HAI_AGENT_RUNTIME_DOWNLOAD_SHA256`, at a trusted build).
## What's next
The fastest path is the [Quickstart](/holo-desktop-cli/getting-started/quickstart). From there, see how to [run it from any host](/holo-desktop-cli/integrations/use-from-cli), [embed it in Python](/holo-desktop-cli/how-to/embed-with-python), or walk a full observe-fix-verify loop in [Find and fix a UI bug with Claude Code](/holo-desktop-cli/examples/claude-code-ui-qa).
# Use HoloDesktop CLI as an ACP sub-agent
Source: https://hub.hcompany.ai/holo-desktop-cli/integrations/use-acp
Delegate desktop tasks to HoloDesktop CLI from an Agent Client Protocol host over stdio.
"ACP" here means the [Agent Client Protocol](https://agentclientprotocol.com) (client to agent) that editors and CLIs use to drive a sub-agent, not the older Agent Communication Protocol that merged into A2A.
ACP support is beta. Use it with hosts that already understand ACP sub-agents, and expect host setup details to change.
When an ACP host delegates to HoloDesktop CLI, it delegates control of the visible desktop. The CLI may open apps, switch focus, click, type, and read whatever is on screen until the task finishes, times out, or is cancelled.
The host starts:
```bash theme={null}
holo acp
```
HoloDesktop CLI starts the desktop runtime locally and creates desktop sessions as the host sends tasks.
## Before you start
Check HoloDesktop CLI from the CLI first:
```bash theme={null}
holo run "Open TextEdit and write a short note saying HoloDesktop CLI is installed"
```
For hosted mode, sign in from a terminal before starting the ACP host:
```bash theme={null}
holo login
holo whoami
```
For local mode, start your OpenAI-compatible server and make sure the host process can read your local model settings:
```bash theme={null}
export HAI_AGENT_RUNTIME_BASE_URL=http://localhost:8000/v1
```
Set `HAI_AGENT_RUNTIME_MODEL` too if your local server requires a model ID, for example `Hcompany/Holo-3.1-35B-A3B`.
That export works for terminal-launched hosts. GUI apps launched from the Dock or Finder usually do not inherit shell exports, so configure the host environment directly if it supports that, or start the host from a shell that already has the variables set. Because ACP hosts are non-interactive stdio processes, sign in before hosted-mode startup, and put local-mode environment variables somewhere the host process can actually read them.
## Configure your host
ACP host configuration differs by host. Use a stdio command that runs the HoloDesktop CLI ACP server:
```text theme={null}
command: holo
args: ["acp"]
```
From a source checkout, run it through `uv` instead:
```text theme={null}
command: uv
args: ["run", "holo", "acp"]
```
## Stop an ACP run
For ACP, the host owns the sub-agent session and HoloDesktop CLI runs as a stdio process. Cancel the session from the host; HoloDesktop CLI cancels the active runtime session on host cancellation or when the stdio connection closes. Bound longer tasks with the runtime's step and time budgets.
## Check it worked
Ask the host to delegate a small desktop task to HoloDesktop CLI. Keep the task specific and self-contained:
```text theme={null}
Open TextEdit and write a short note saying HoloDesktop CLI is connected through ACP. Return "done" when the note is visible.
```
If the host fails during startup, check:
* hosted mode has `HAI_API_KEY` available;
* local mode has `HAI_AGENT_RUNTIME_BASE_URL` available to the host process;
* the command path points at the same checkout where `uv sync` succeeded;
* runtime logs under `~/.holo/logs/`.
## ACP or MCP?
Use MCP when your host expects tools. Use ACP when your host expects sub-agents. Both surfaces start the same local desktop runtime; the difference is how the host talks to HoloDesktop CLI.
## What's next
Use [MCP](/holo-desktop-cli/integrations/use-mcp) if your host supports MCP but not ACP.
# Use HoloDesktop CLI as a skill
Source: https://hub.hcompany.ai/holo-desktop-cli/integrations/use-as-skill
Understand the HoloDesktop CLI skill that gets installed into supported agent hosts.
Some hosts can load skills: small instruction packages that teach the host when and how to use an external tool. HoloDesktop CLI ships a `holo-desktop` skill for hosts with skill loading.
The skill does not replace MCP or ACP. It gives the host better instructions for deciding when to call the CLI and how to write a useful task string.
## Install the skill
Use `holo install` for hosts that support skill auto-loading:
```bash theme={null}
cd /path/to/your/claude-code-workspace
holo install claude-code
```
or install into every detected supported host:
```bash theme={null}
holo install
```
If Claude Code is detected, run this from the workspace where Claude Code should use the CLI.
When the host supports skills, `holo install` links or copies the bundled `holo-desktop` skill into that host's skill directory.
For Claude Code, the skill is installed under `~/.claude/skills/`, while the MCP server registration uses Claude Code's default local scope for the workspace where you ran the command.
Known skill locations include:
| Host | Skill location |
| ----------- | ---------------------------------------- |
| Claude Code | `~/.claude/skills/holo-desktop` |
| Codex | `~/.agents/skills/holo-desktop` |
| Grok Build | `~/.grok/skills/holo-desktop` |
| OpenClaw | `~/.openclaw/skills/holo-desktop` |
| OpenCode | `~/.config/opencode/skills/holo-desktop` |
Hosts that do not support skill loading still use the CLI through MCP config.
## What the skill teaches
The skill tells the host that the CLI is useful for work that has to happen on the user's real machine:
* operating native apps;
* navigating system UI;
* using the user's logged-in browser profile;
* reading what is visible on screen;
* completing GUI work that the host cannot do through files, shell commands, APIs, or web fetches.
It also tells the host to pass a self-contained task string. The CLI does not see the host conversation, previous tool calls, or hidden context, so the host has to include the relevant app, workspace, account, person, and success condition in the `task`.
## Safety boundary
The CLI works in the user's real apps with the user's real data. For actions the user might regret, such as sending messages, deleting data, paying, or changing system settings, the host should confirm with the user first or ask the CLI to observe and report instead.
Do not rely on a skill alone as a safety boundary. The host should still ask before irreversible actions, and the task you pass should say exactly what may be changed.
## Check it worked
After installing, restart the host if needed and ask for a task that clearly requires the desktop, such as:
```text theme={null}
Use HoloDesktop CLI to open TextEdit and write a short note saying the HoloDesktop CLI skill is installed.
```
If the host does not mention or use the CLI, check whether the host supports skill loading and whether the `holo-desktop` skill exists in the expected directory.
## What's next
Use [MCP](/holo-desktop-cli/integrations/use-mcp) for the tool connection that the skill usually points the host toward.
# Use HoloDesktop CLI from the terminal
Source: https://hub.hcompany.ai/holo-desktop-cli/integrations/use-from-cli
Run desktop tasks directly from your terminal with holo run.
Running a task from the terminal is the most direct way to use HoloDesktop CLI, and the quickest way to check it works before you wire it into an agent host. One `holo run` and the agent is operating your desktop.
## Before you start
Complete the [Quickstart](/holo-desktop-cli/getting-started/quickstart), then choose a [hosted or local model](/holo-desktop-cli/getting-started/hosted-or-local-models).
For hosted mode:
```bash theme={null}
holo whoami
```
For local mode, make sure your OpenAI-compatible server is running.
## Run a task
Hosted mode uses H Company's Models API:
```bash theme={null}
holo run "Open TextEdit and write a short note saying HoloDesktop CLI is installed"
```
Local mode uses your server:
```bash theme={null}
holo run \
--base-url http://localhost:8000/v1 \
"Open TextEdit and write a short note saying HoloDesktop CLI is installed"
```
Add `--model` when the backend needs a specific model.
For hosted mode, use H Company API model IDs such as `holo3-1-35b-a3b` or `holo3-122b-a10b`:
```bash theme={null}
holo run \
--model holo3-1-35b-a3b \
"Open TextEdit and write a short note saying HoloDesktop CLI is installed"
```
Local mode uses the model ID exposed by your local server. For example, a server running the Holo 3.1 35B checkpoint might expose `Hcompany/Holo-3.1-35B-A3B`.
```bash theme={null}
holo run \
--base-url http://localhost:8000/v1 \
--model Hcompany/Holo-3.1-35B-A3B \
"Open TextEdit and write a short note saying HoloDesktop CLI is installed"
```
## Useful options
| Option | Use it when |
| -------------- | ------------------------------------------------------------ |
| `--base-url` | You want local mode with an OpenAI-compatible server |
| `--model` | Your hosted or local backend needs a specific model ID |
| `--max-steps` | You want to cap how many agent steps the CLI can take |
| `--max-time-s` | You want a wall-clock timeout |
| `--runs-dir` | You want run event logs somewhere other than `~/.holo/runs/` |
| `--profile` | You want per-step timing output at the end of the run |
| `--quiet` | You only want the final answer printed |
## Write good task strings
The agent only sees the task string you pass to `holo run`. Include the app, the account or workspace when it matters, the action to take, and what success looks like.
Good:
```bash theme={null}
holo run "Open Calendar, show today's events, and report each event with its time and title. Do not create or edit anything."
```
Too vague:
```bash theme={null}
holo run "What's on today?"
```
## Check it worked
The task prints progress in the terminal, then a final answer. Run logs are written under:
```text theme={null}
~/.holo/runs/
```
Runtime startup logs are written under:
```text theme={null}
~/.holo/logs/
```
## What's next
Use [MCP](/holo-desktop-cli/integrations/use-mcp) when you want Claude Code, Cursor, Codex, or another host to call the CLI as a tool.
# Use HoloDesktop CLI as an MCP server
Source: https://hub.hcompany.ai/holo-desktop-cli/integrations/use-mcp
Expose HoloDesktop CLI to MCP-capable hosts such as Claude Code, Cursor, Codex, Grok Build, and OpenCode.
The host starts `holo mcp` over stdio, and the CLI exposes one tool named `holo_desktop` that runs each task on the local desktop runtime. MCP support is beta, and host implementations are still moving, especially around cancellation and long-running desktop tool calls.
When an MCP host calls HoloDesktop CLI, it delegates control of the visible desktop. The CLI may open apps, switch focus, click, type, and read whatever is on screen until the task finishes, times out, or is cancelled.
## Before you start
Run one CLI task first to check that HoloDesktop CLI starts on this machine:
```bash theme={null}
holo run "Open TextEdit and write a short note saying HoloDesktop CLI is installed"
```
For hosted mode, sign in before installing HoloDesktop CLI into a host:
```bash theme={null}
holo login
holo whoami
```
MCP hosts launch HoloDesktop CLI non-interactively, so they cannot complete browser login during startup.
For local mode, make sure the host process can read your local model settings:
```bash theme={null}
export HAI_AGENT_RUNTIME_BASE_URL=http://localhost:8000/v1
```
Set `HAI_AGENT_RUNTIME_MODEL` too if your local server needs a model ID, for example `Hcompany/Holo-3.1-35B-A3B`.
That export works for terminal-launched hosts such as Claude Code, Codex, OpenCode, or other MCP clients you start from the same shell. GUI apps launched from the Dock or Finder usually do not inherit shell exports. For those hosts, add the variables to the host's MCP config if it supports an `env` block, or start the host from a shell that already has the variables set.
## List supported hosts
```bash theme={null}
holo install list
```
This prints supported host IDs and whether each host is detected on your machine.
Supported host IDs include:
| Host ID | Host |
| ---------------- | ------------------ |
| `claude-code` | Claude Code |
| `claude-desktop` | Claude Desktop |
| `codex` | Codex |
| `cursor` | Cursor |
| `grok-build` | Grok Build |
| `opencode` | OpenCode |
| `openclaw` | OpenClaw |
| `hermes` | Hermes |
| `copilot` | GitHub Copilot CLI |
| `antigravity` | Antigravity |
## Install into one host
Install into Claude Code from the workspace where Claude Code should use HoloDesktop CLI:
```bash theme={null}
cd /path/to/your/claude-code-workspace
holo install claude-code
```
Claude Code's MCP CLI defaults to `local` scope. HoloDesktop CLI calls that CLI, so the MCP server is registered for the current Claude Code workspace. Claude stores the local-scoped entry in its own config, associated with that project path, rather than in a HoloDesktop CLI-managed `mcp.json` file.
Install into Cursor:
```bash theme={null}
holo install cursor
```
Install into Grok Build:
```bash theme={null}
holo install grok-build
```
This registers the `holo` MCP server in `~/.grok/config.toml` through Grok Build's own CLI and installs the bundled skill under `~/.grok/skills/holo-desktop`.
Check that Grok Build loaded both integrations:
```bash theme={null}
grok mcp list
grok inspect
```
Install into every detected supported host:
```bash theme={null}
holo install
```
If Claude Code is detected, HoloDesktop CLI uses Claude Code's local scope for the current workspace.
`holo install` either calls the host's own MCP CLI or updates the host's MCP config file. When possible, it writes the absolute path to the `holo` executable so GUI hosts do not depend on your shell `PATH`.
For file-backed hosts such as Cursor, the installed entry looks like this:
```json theme={null}
{
"mcpServers": {
"holo": {
"type": "stdio",
"command": "/absolute/path/to/holo",
"args": ["mcp"]
}
}
}
```
For local mode in a GUI host, add environment variables in the shape that host supports, for example:
```json theme={null}
{
"mcpServers": {
"holo": {
"type": "stdio",
"command": "/absolute/path/to/holo",
"args": ["mcp"],
"env": {
"HAI_AGENT_RUNTIME_BASE_URL": "http://localhost:8000/v1",
"HAI_AGENT_RUNTIME_MODEL": "Hcompany/Holo-3.1-35B-A3B"
}
}
}
}
```
Re-running `holo install` preserves extra keys such as `env` while refreshing the command and args.
## Stop an MCP run
For MCP, the host owns the chat UI and HoloDesktop CLI runs as a stdio tool server. Cancel the in-progress tool call from the host; HoloDesktop CLI cancels the active runtime session when it receives the MCP `notifications/cancelled` signal or when the host closes the stdio connection. Some hosts do not propagate cancellation reliably yet, so prefer short tasks and bound them with the runtime's step and time budgets.
## What the host gets
The MCP server exposes one tool:
```text theme={null}
holo_desktop(task: str) -> str
```
`holo_desktop` is a blocking tool call, not a background job. While it is running, HoloDesktop CLI owns one desktop task and may keep observing, clicking, and typing until the task completes or reaches its safety budget.
MCP defines request cancellation with a `notifications/cancelled` message for an in-progress request. Current HoloDesktop CLI clients use that signal, and stdio-server shutdown, to cancel the active runtime session before the tool call unwinds. Some MCP hosts do not propagate cancellation consistently yet, so stopping a chat response may not stop the desktop run right away. We are working with upstream host providers to improve this. For now, keep HoloDesktop CLI up to date and prefer short, specific tasks that can finish or time out cleanly.
Each call should contain a self-contained desktop task. The calling host should include the context HoloDesktop CLI needs: the app, workspace, account, person, and success condition.
Good task:
```text theme={null}
Open Slack in the Acme workspace and send a DM to Sarah Chen saying "on my way". Return "sent" after the message appears.
```
Too vague:
```text theme={null}
Tell Sarah.
```
## Confirm the host picked it up
Restart the host after installation if it was already open. Then ask the host to do a small, safe desktop task, such as opening a text editor and writing a short note.
If the host cannot find HoloDesktop CLI:
* run `holo install list` and confirm the host ID;
* re-run `holo install `;
* check that hosted mode has `HAI_API_KEY` available, or local mode has `HAI_AGENT_RUNTIME_BASE_URL` available to the host process;
* check `~/.holo/logs/` for runtime startup errors.
## What's next
Use [HoloDesktop CLI as a skill](/holo-desktop-cli/integrations/use-as-skill) to understand the guidance installed into hosts that support skill loading.
# CLI reference
Source: https://hub.hcompany.ai/holo-desktop-cli/reference/cli
Common HoloDesktop CLI commands and options.
Run HoloDesktop CLI commands with the `holo` command. If you are developing from a source checkout instead of using the installer, prefix commands with `uv run`.
```bash theme={null}
holo --help
```
## Commands
| Command | Purpose |
| -------------- | --------------------------------------------------------------- |
| `holo run` | Run one foreground task on the visible desktop. |
| `holo stop` | Stop the running turn; `--force` also kills the runtime. |
| `holo mcp` | Run the stdio MCP server. |
| `holo acp` | Run the stdio ACP server. |
| `holo install` | Wire HoloDesktop CLI into a supported host. |
| `holo login` | Sign in to H Company for hosted mode. |
| `holo whoami` | Print the cached hosted-mode identity. |
| `holo doctor` | Diagnose runtime, login, agent API, permissions, and `~/.holo`. |
| `holo serve` | Run the local A2A server. |
## Run
```bash theme={null}
holo run "Open TextEdit and write a short note saying HoloDesktop CLI is installed"
```
Useful options:
| Option | Purpose |
| ---------------------- | --------------------------------------------------------------- |
| `--base-url URL` | Use a local OpenAI-compatible model endpoint. |
| `--model NAME` | Select the hosted model ID or local model ID. |
| `--max-steps N` | Stop after at most `N` agent steps. |
| `--max-time-s SECONDS` | Stop after a wall-clock timeout. |
| `--runs-dir DIR` | Write runtime run logs somewhere other than the binary default. |
| `--port PORT` | Use a non-default agent API port. Defaults to `18795`. |
| `--quiet` | Print only the final answer. |
| `--profile` | Print timing output from the runtime event log at exit. |
| `--expand` | Print every step as a full panel. |
Hosted mode:
```bash theme={null}
holo run \
--model holo3-1-35b-a3b \
"Open TextEdit and write a short note saying HoloDesktop CLI is installed"
```
Local mode:
```bash theme={null}
holo run \
--base-url http://localhost:8000/v1 \
--model Hcompany/Holo-3.1-35B-A3B \
"Open TextEdit and write a short note saying HoloDesktop CLI is installed"
```
If a runtime is already listening on the same port, `--model`, `--base-url`, and `--runs-dir` require a fresh runtime. Stop the existing runtime or pass a different `--port`.
## Stop a run
While a foreground `holo run` is active, press `Esc` twice quickly to stop it. This is a global kill switch, so it works even when Holo holds focus and you cannot reach the terminal (on macOS it needs Input Monitoring permission). You can also press `Ctrl+C` in the launching terminal, or run `holo stop` from any other terminal; `holo stop --force` additionally kills the runtime. For host-launched runs, run `holo stop` or cancel the request from your MCP or ACP host. Bound risky tasks ahead of time with `--max-steps` or `--max-time-s`.
## MCP
```bash theme={null}
holo mcp
```
This command is normally launched by an MCP host, not typed directly. It uses stdio and auto-spawns `hai-agent-runtime` if no healthy runtime is already listening.
Use [Use HoloDesktop CLI as an MCP server](/holo-desktop-cli/integrations/use-mcp) for host setup.
## ACP
```bash theme={null}
holo acp
```
This command is normally launched by an ACP host. It uses stdio and spawns the runtime on first use.
Use [Use HoloDesktop CLI as an ACP sub-agent](/holo-desktop-cli/integrations/use-acp) for host setup.
## Install
```bash theme={null}
holo install claude-code
```
For Claude Code, run this from the workspace where you want HoloDesktop CLI registered. Claude Code's MCP configuration is workspace-scoped.
## Login
```bash theme={null}
holo login
```
`holo login` opens a browser sign-in flow, saves `HAI_API_KEY` to `~/.holo/.env`, and writes an identity cache to `~/.holo/profile.json`.
Rotate or switch identity:
```bash theme={null}
holo login --force
```
## Whoami
```bash theme={null}
holo whoami
```
This reads local identity state and prints the signed-in account. It exits non-zero if no hosted-mode identity is available.
## Doctor
```bash theme={null}
holo doctor
```
`doctor` is read-only. Use it when setup, runtime launch, credentials, permissions, or the agent API are failing.
# Environment variables
Source: https://hub.hcompany.ai/holo-desktop-cli/reference/environment-variables
Environment variables used by HoloDesktop CLI and hai-agent-runtime.
Most users only need `HAI_API_KEY` for hosted mode or `HAI_AGENT_RUNTIME_BASE_URL` for local mode. The rest are useful for host integrations, debugging, or advanced runtime control.
## Model and credentials
| Variable | Purpose |
| ---------------------------- | --------------------------------------------------------------------------- |
| `HAI_API_KEY` | Hosted-mode API key for H Company's Models API. |
| `HAI_AGENT_RUNTIME_BASE_URL` | OpenAI-compatible local model endpoint, such as `http://localhost:8000/v1`. |
| `HAI_AGENT_RUNTIME_MODEL` | Model ID passed to the runtime, such as `Hcompany/Holo-3.1-35B-A3B`. |
`holo login` stores `HAI_API_KEY` in:
```text theme={null}
~/.holo/.env
```
The client loads environment in this order:
1. process environment;
2. `~/.holo/.env`;
3. `.env` in the current working directory.
Process environment wins over files.
## Runtime
| Variable | Purpose |
| ----------------------------- | ------------------------------------------------------------------------------------------------- |
| `HAI_AGENT_RUNTIME_PORT` | Agent API port. Defaults to `18795`. |
| `HAI_AGENT_RUNTIME_API_TOKEN` | Bearer token for the loopback agent API. If unset, the client generates one for spawned runtimes. |
| `HAI_AGENT_RUNTIME_RUNS_DIR` | Runtime event-log directory. If unset, the runtime default is `~/.holo/runs`. |
| `HAI_AGENT_RUNTIME_FAKE` | Test mode flag. Values `1`, `true`, or `yes` enable fake-agent mode. |
| `HOLO_AUTH_TOKEN` | Bearer token clients present to the `holo serve` A2A server. |
For host integrations, set model variables in the host's configuration if the host is launched from a GUI and does not inherit your shell exports.
Example MCP environment:
```json theme={null}
{
"mcpServers": {
"holo": {
"command": "holo",
"args": ["mcp"],
"env": {
"HAI_AGENT_RUNTIME_BASE_URL": "http://localhost:8000/v1",
"HAI_AGENT_RUNTIME_MODEL": "Hcompany/Holo-3.1-35B-A3B"
}
}
}
}
```
## Runtime download overrides
These are for tests, operations, or trusted development builds:
| Variable | Purpose |
| ----------------------------------- | ------------------------------------------------------------- |
| `HAI_AGENT_RUNTIME_DOWNLOAD_URL` | Override the managed runtime download URL. |
| `HAI_AGENT_RUNTIME_DOWNLOAD_SHA256` | Required sha256 when `HAI_AGENT_RUNTIME_DOWNLOAD_URL` is set. |
HoloDesktop CLI refuses an unverified download URL. Set both values or neither.
## Development and tests
Some repository tests use opt-in variables such as `HOLO_RUN_INTEGRATION`, `HOLO_IT_BASE_URL`, and `HOLO_IT_MODEL`. These are test harness variables, not end-user configuration.
For normal local model use, prefer:
```bash theme={null}
holo run \
--base-url http://localhost:8000/v1 \
--model Hcompany/Holo-3.1-35B-A3B \
"Open TextEdit and write a short note saying HoloDesktop CLI is installed"
```
For host-launched stdio servers, use:
```bash theme={null}
export HAI_AGENT_RUNTIME_BASE_URL=http://localhost:8000/v1
export HAI_AGENT_RUNTIME_MODEL=Hcompany/Holo-3.1-35B-A3B
```
# Paths and files
Source: https://hub.hcompany.ai/holo-desktop-cli/reference/paths-and-files
Local files, logs, runtime cache, skills, and run artifacts used by HoloDesktop CLI.
HoloDesktop CLI stores local state under:
```text theme={null}
~/.holo/
```
Treat this directory as sensitive. It can contain credentials, identity metadata, instructions, skills, screenshots, and run logs.
## Core paths
| Path | Purpose |
| -------------------------- | ------------------------------------------------------- |
| `~/.holo/.env` | Hosted-mode `HAI_API_KEY` written by `holo login`. |
| `~/.holo/profile.json` | Cached identity metadata used by `holo whoami`. |
| `~/.holo/runtime/` | Managed `hai-agent-runtime` installs. |
| `~/.holo/logs/` | Runtime startup and stderr logs. |
| `~/.holo/runs/` | Runtime run artifacts. |
| `~/.holo/desktop.lock` | Advisory lock so one desktop task runs at a time. |
| `~/.holo/skills/` | Installed HoloDesktop CLI skills. |
| `~/.holo/settings.json` | Client settings such as seeded skills. |
| `~/.holo/agents.md` | Standing HoloDesktop CLI instructions. |
| `~/.holo/memories.md` | General memories loaded into HoloDesktop CLI sessions. |
| `~/.holo/holo-memories.md` | HoloDesktop CLI-specific memories loaded into sessions. |
| `~/.holo/rules.md` | Rules loaded into sessions. |
## Runtime cache
When `hai-agent-runtime` is not on `PATH`, HoloDesktop CLI downloads the pinned runtime version to:
```text theme={null}
~/.holo/runtime//
```
The download is sha256-verified before install. On macOS, the executable lives inside the downloaded app bundle:
```text theme={null}
~/.holo/runtime//hai_agent_runtime.app/Contents/MacOS/hai-agent-runtime
```
HoloDesktop CLI resolves runtime binaries in this order:
1. `hai-agent-runtime` on `PATH`;
2. managed install under `~/.holo/runtime/`;
3. download-on-first-run.
## Logs
Runtime startup logs are written under:
```text theme={null}
~/.holo/logs/
```
The file name includes the agent API port:
```text theme={null}
hai-agent-runtime-18795.log
```
Use these logs for startup failures, runtime crashes, model backend errors, port conflicts, and permission-looking errors.
## Run artifacts
Default run artifacts live under:
```text theme={null}
~/.holo/runs/
```
Override the run directory for a single CLI run:
```bash theme={null}
holo run \
--runs-dir /tmp/holo-runs \
"Open TextEdit and write the word test"
```
The most important file is:
```text theme={null}
events.jsonl
```
Each line is one runtime event. Observation events can include base64 JPEG screenshots of the visible desktop. Do not publish run directories without reviewing them.
## Token files
When HoloDesktop CLI spawns a runtime and no explicit `HAI_AGENT_RUNTIME_API_TOKEN` is set, it generates a bearer token and publishes it locally so other local clients can attach to the same runtime:
```text theme={null}
~/.holo/agent-token-
```
The token file is removed when the owning client closes the spawned runtime.
## Skills
Skills are installed under:
```text theme={null}
~/.holo/skills//SKILL.md
```
Each skill directory name becomes the skill name HoloDesktop CLI sees. Keep directory names lowercase and hyphenated. To author skills, memories, and rules, see [Customize with skills, memories, and rules](/holo-desktop-cli/how-to/customize).
Bundled skills can be seeded automatically by the client, and examples may install their own skills. The expense-report example installs:
```text theme={null}
~/.holo/skills/expense-report/SKILL.md
```
## Host config files
Host-specific config locations depend on the host.
For Claude Code, project-local MCP config is associated with the workspace where you run:
```bash theme={null}
holo install claude-code
```
Run install commands from the workspace where you expect the host to discover HoloDesktop CLI.
# Security and privacy
Source: https://hub.hcompany.ai/holo-desktop-cli/security-and-privacy
Understand what HoloDesktop CLI sends to H Company, what stays local, and how hosted and local model modes differ.
HoloDesktop CLI operates your real desktop. It can observe visible windows, reason over what it sees, click, type, scroll, and interact with local apps.
The main privacy question is where the model runs.
## What H Company receives
| Mode | What can be sent to H Company | What the CLI does not upload |
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| Hosted models | Task text, rendered instructions, user-provided context, action history, and visual observations needed by H Company's hosted model service. | Your local run artifact directory, local runtime logs, and local files that are not part of the task context. |
| Local model endpoint | Nothing for model inference, if the endpoint is running on your own machine. | Task text, screenshots, model inputs, local run artifacts, and local runtime logs. |
| Remote model endpoint you control | The remote endpoint receives the model inputs instead of H Company. | H Company still does not receive the model inference payload from that run. |
In other words, the privacy boundary follows the model endpoint: hosted mode sends task-relevant model inputs to H Company's Models API, and local mode sends them to the endpoint you provide.
## What the agent can see
During a run, the CLI observes the visible desktop so it can decide what to do next. Observations can include anything visible on screen:
* apps and browser tabs;
* local documents;
* chat or email content;
* account names and workspace names;
* file paths;
* permission dialogs;
* secrets or personal information visible in the foreground.
Close or hide anything the agent should not see before starting a run.
Anything visible on screen can become task context. Close unrelated tabs, documents, chats, password managers, and admin consoles before a sensitive run.
## What H Company retains
The CLI itself does not upload a recording of your desktop, your local run artifacts, or your local runtime logs to H Company storage.
When you use hosted models, H Company's hosted model service receives the task-relevant inference payload for the run. That payload can include text instructions, contextual files or memories the CLI included, tool and event history, and visual observations of the desktop. Retention for hosted model requests is governed by the Models API terms and the agreement for your H Company organization.
When you use a local model endpoint running on your own machine, H Company does not receive the task text, screenshots, or model inputs for that run.
## Local artifacts
The CLI keeps diagnostic artifacts on your machine so you can inspect and debug runs. Those artifacts can include task text, event history, screenshots, model outputs, tool results, local paths, and app state that appeared during the run.
These files are local diagnostics, not an analytics upload. They are useful for debugging but can be sensitive, so review and redact them before sharing, and prefer redacted snippets over whole run directories when posting in an issue or attaching them to support threads.
For exact paths, see [Paths and files](/holo-desktop-cli/reference/paths-and-files). For event structure, see [Debug a failed run](/holo-desktop-cli/how-to/debug-failed-run).
## Host integrations
When HoloDesktop CLI runs through MCP, ACP, or a skill, the host decides when to call it and what task text to pass into the run.
Before enabling a host integration, check:
* whether the host asks before tool calls or can invoke tools automatically;
* whether the configuration is project-local or global;
* whether the prompt gives the CLI permission to operate unrelated apps;
* whether the visible desktop contains information outside the task.
The privacy boundary does not change because the call came from a host. Hosted model mode still sends model inputs to H Company. Local model mode still sends model inputs to your configured endpoint.
## Practical checklist
Before a sensitive run:
1. Close unrelated windows and tabs.
2. Use local model mode if task context should not go to H Company's hosted model service.
3. Use a dedicated browser profile for sensitive web-app testing.
4. Keep prompts narrow and task-specific.
5. Review local artifacts before sharing them.
6. Rotate credentials if you accidentally publish logs, screenshots, or run artifacts.
# H Tech Hub
Source: https://hub.hcompany.ai/index
Build computer-use AI on H Company's **Holo** Vision-Language Models. Pick the product that fits how you want to work: call the models directly, run them on your own machine, or let H run fully managed agents for you.
## Computer-Use Agents
Describe a task in plain language; H provisions the environment, runs the agent on top of Holo, and returns the result through a lifecycle you can monitor, steer, and stop.
What managed Computer-Use Agents are and how they fit together.
Create your first agent session in under five minutes.
## Models API
Send screenshots and prompts to a single OpenAI-compatible API, and get back structured actions and click coordinates to drive any web, desktop, or mobile interface.
Run Holo in five minutes.
What the API does and the models on offer.
Multi-turn control for an autonomous agent.
Get click coordinates from a screenshot.
## HoloDesktop CLI
Run Holo models on your local computer, then use them from Claude Code, MCP hosts, ACP agents, or the command line.
Install the CLI and run a first task on your desktop.
## Use these docs from AI tools
Connect Claude, Cursor, or any MCP client to this site so it can search and read the docs while answering your questions.
## Models and research
Mobile, function calling, and local inference.
78.85% on OSWorld-Verified.
Model cards, weights, and quantized builds.
Create a key on the H Platform. Free tier included.
## HoloTab
Try Holo in your browser
Run Holo in your browser, no code required.
# Models
Source: https://hub.hcompany.ai/models
The Holo models served by the Models API: capabilities, pricing, and lifecycle.
The Models API serves two Holo models. This page is the single source of truth for what is available; you can also query it programmatically with [`GET /v1/models`](/models-api/list-models).
| Model ID | Architecture | Context | Max output | Input / output per 1M tokens | Native function calling | License |
| :---------------- | :--------------------- | :------ | :--------- | :--------------------------- | :---------------------- | :------------ |
| `holo3-1-35b-a3b` | MoE, 35B / 3B active | 65,536 | 4,096 | $0.25 / $1.80 | Yes | Apache 2.0 |
| `holo3-122b-a10b` | MoE, 122B / 10B active | 65,536 | 32,768 | $0.40 / $3.00 | No | Research only |
Both models accept text + images (JPEG, PNG, WebP; up to 5 images per request) and support the reasoning channel and [structured outputs](/agent-loop#output-format-and-tool-calls).
Fast, low-latency computer use across web, desktop, and mobile. Free tier (rate-limited, 10 RPM). Open weights on Hugging Face.
Maximum performance for complex tasks. Paid tier only. API-only: weights are not published; see the blog post for benchmarks.
## Choosing a model
* Start with `holo3-1-35b-a3b`: it is on the free tier, supports both output formats (structured outputs and native `tool_calls`), and its latency suits interactive agent loops.
* Switch to `holo3-122b-a10b` when task complexity dominates: long multi-step navigation, dense reasoning, or when the 35B's 4,096-token output cap is too tight (for example long [document transcriptions](/document-ocr)). It supports structured outputs but not native function calling.
## Open weights and local inference
`holo3-1-35b-a3b` corresponds to the open-weight Holo3.1-35B-A3B release. The [Holo3.1 collection on Hugging Face](https://huggingface.co/collections/Hcompany/holo31) also carries the other family sizes (0.8B, 4B, 9B) and quantized FP8, GGUF, and NVFP4 builds; those are for self-hosting and are not served by this API. See [run a local model server](/holo-desktop-cli/how-to/run-a-local-model-server) for a vLLM setup.
## Model lifecycle
Model IDs are stable identifiers. When a model is scheduled for removal, its [`deprecation_date`](/models-api/list-models) field is set in `GET /v1/models` and a notice appears here; after removal, requests to the old ID fail with a `model_not_found` error. Pin a model ID in production and check `deprecation_date` when you upgrade.
## Rate limits and billing
Rate-limited access to `holo3-1-35b-a3b` (10 requests per minute) without a credit card. Create a key on [Portal-H](https://portal.hcompany.ai/?product=modelsapi\&source=docs).
Higher rate limits plus access to `holo3-122b-a10b`. Add credits on [Portal-H](https://portal.hcompany.ai/credits?product=modelsapi\&source=docs). Billing is per model, per million input and output tokens; the API uses zero data retention by default. Current rates and FAQ: [Models API pricing](https://hcompany.ai/holo-models-api).
# Create chat completion
Source: https://hub.hcompany.ai/models-api/chat-completions
POST https://api.hcompany.ai/v1/chat/completions
OpenAI-compatible chat completion with Holo-specific structured outputs and reasoning.
The single inference endpoint. It is OpenAI-compatible: the official OpenAI clients work as-is with `base_url` pointed at `https://api.hcompany.ai/v1/`. Holo-specific behavior (structured outputs, the reasoning toggle) is controlled by extra body fields documented below.
**Returns** a chat completion object, or a stream of chunk objects when `stream` is `true`.
***
## Body parameters
Model ID to run. One of the IDs listed on the [Models](/models) page, e.g. `holo3-1-35b-a3b`.
The conversation so far. Standard OpenAI message objects (`role`, `content`); `content` can be a string or an array of `text` and `image_url` parts. Images accept HTTPS URLs or base64 data URIs (JPEG, PNG, WebP), up to 5 per request.
Holo-specific. Constrain the response, at the decoding level, to a JSON object matching a schema: pass `{"json": }`. The object is returned in `message.content`. Use this for the [structured-output agent loop](/agent-loop) and [element localization](/element-localization).
With the OpenAI SDKs, pass this (and `chat_template_kwargs`) via `extra_body` in Python or an untyped spread in TypeScript; the SDK merges them into the request body. On the raw wire they are top-level fields, as in the cURL example. The API silently ignores a body nested under a literal `"extra_body"` key.
Holo-specific. `{"enable_thinking": bool}` toggles the reasoning channel. Use `true` for agent loops (Holo plans before acting), `false` for single-shot calls like grounding and OCR.
How much the model plans before acting: `"low"`, `"medium"`, or `"high"`. `"medium"` is a sensible default for agent loops.
OpenAI-style function declarations for [native function calling](/agent-loop#output-format-and-tool-calls). Supported by `holo3-1-35b-a3b` only. Set `tool_choice: "required"` so the model acts on every step, and do not mix with `structured_outputs`.
Standard OpenAI semantics. Use `"required"` in function-calling agent loops.
Stream the response as server-sent chunk events. Reasoning tokens arrive in `delta.reasoning`, content in `delta.content`.
Output cap for this request. The hard per-model ceilings differ: 4,096 for `holo3-1-35b-a3b`, 32,768 for `holo3-122b-a10b` (see [Models](/models)).
Sampling temperature. Use `0.0` for deterministic single-shot calls ([localization](/element-localization), [OCR](/document-ocr)); `0.8` works well in agent loops. Also supported: `top_p`, `top_k`, `stop`, `frequency_penalty`, `presence_penalty`, `seed`.
***
## Response
The action or answer: the constrained JSON object (structured-output mode) or the assistant text. `null` when the model responded with `tool_calls` only.
The thinking trace, present when thinking is enabled. Read it for visibility; do not feed it back into the conversation. The chat template drops it between turns, so anything the model must remember has to flow through `content`. See the [Agent loop](/agent-loop#reasoning) for carrying state forward.
Present in native function-calling mode only. Each call carries an `id` and a `function` object with `name` and a JSON-encoded `arguments` string.
`stop`, `length` (hit `max_tokens` or the model ceiling), or `tool_calls`.
`prompt_tokens`, `completion_tokens`, `total_tokens` for the request.
***
## Examples
```python Python theme={null}
import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.hcompany.ai/v1/",
api_key=os.environ["HAI_API_KEY"],
)
resp = client.chat.completions.create(
model="holo3-1-35b-a3b",
messages=[{"role": "user", "content": "In one sentence, what is a computer-use agent?"}],
reasoning_effort="medium",
extra_body={"chat_template_kwargs": {"enable_thinking": True}},
)
print(resp.choices[0].message.content)
```
```typescript TypeScript theme={null}
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.hcompany.ai/v1/",
apiKey: process.env.HAI_API_KEY,
});
const resp = await client.chat.completions.create({
model: "holo3-1-35b-a3b",
messages: [{ role: "user", content: "In one sentence, what is a computer-use agent?" }],
reasoning_effort: "medium",
...({ chat_template_kwargs: { enable_thinking: true } } as any),
});
console.log(resp.choices[0].message.content);
```
```bash cURL theme={null}
curl https://api.hcompany.ai/v1/chat/completions \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "holo3-1-35b-a3b",
"messages": [{"role": "user", "content": "In one sentence, what is a computer-use agent?"}],
"reasoning_effort": "medium",
"chat_template_kwargs": {"enable_thinking": true}
}'
```
### Streaming
```python Python theme={null}
stream = client.chat.completions.create(
model="holo3-1-35b-a3b",
messages=[{"role": "user", "content": "In one sentence, what is a computer-use agent?"}],
stream=True,
)
for chunk in stream:
delta = chunk.choices[0].delta
if delta.content:
print(delta.content, end="", flush=True)
```
```typescript TypeScript theme={null}
const stream = await client.chat.completions.create({
model: "holo3-1-35b-a3b",
messages: [{ role: "user", content: "In one sentence, what is a computer-use agent?" }],
stream: true,
});
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta;
if (delta?.content) process.stdout.write(delta.content);
}
```
# List models
Source: https://hub.hcompany.ai/models-api/list-models
GET https://api.hcompany.ai/v1/models
Programmatic discovery of the served models, their capabilities, pricing, and deprecation dates.
Lists the models currently served by the API, with capabilities, limits, pricing, and lifecycle metadata. Use it to discover model IDs at runtime and to detect upcoming removals via `deprecation_date` instead of hardcoding assumptions.
**Returns** a list object whose `data` array contains one object per model.
***
## Response
The model ID to pass as `model` in [chat completions](/models-api/chat-completions), e.g. `holo3-1-35b-a3b`.
Total context window in tokens.
Hard ceiling on output tokens per request: 4,096 for `holo3-1-35b-a3b`, 32,768 for `holo3-122b-a10b`.
`["text", "image"]` for both Holo models.
Capability flags. `reasoning` on both models; `tools` (native function calling) on `holo3-1-35b-a3b` only.
Accepted sampling fields, e.g. `temperature`, `top_p`, `top_k`, `max_tokens`, `stop`, `frequency_penalty`, `presence_penalty`, `seed`.
Per-token USD rates as decimal strings: `prompt` and `completion` per input/output token.
Set when the model is scheduled for removal; `null` otherwise. After removal, requests to the ID fail with `model_not_found`.
Whether the model is currently serving traffic (also see `is_ready`).
***
## Examples
```python Python theme={null}
models = client.models.list()
for m in models.data:
print(m.id)
```
```typescript TypeScript theme={null}
const models = await client.models.list();
for (const m of models.data) {
console.log(m.id);
}
```
```bash cURL theme={null}
curl https://api.hcompany.ai/v1/models \
-H "Authorization: Bearer $HAI_API_KEY"
```
```json Response (truncated) theme={null}
{
"object": "list",
"data": [
{
"id": "holo3-1-35b-a3b",
"object": "model",
"name": "Holo3 1 35B A3B",
"context_length": 65536,
"max_output_length": 4096,
"input_modalities": ["text", "image"],
"supported_features": ["reasoning", "tools"],
"supported_sampling_parameters": ["temperature", "top_p", "top_k", "max_tokens", "stop", "frequency_penalty", "presence_penalty", "seed"],
"pricing": {"prompt": "0.00000025", "completion": "0.0000018"},
"is_active": true,
"deprecation_date": null
}
]
}
```
# Quickstart
Source: https://hub.hcompany.ai/quickstart
Get from zero to your first Holo request in three steps. If you want the model lineup, capabilities, and pricing first, see [Models](/models); for what Holo is and the ways to use it, see the [overview](/about-the-models-api).
## Get started
Generate a key on [Portal-H](https://portal.hcompany.ai/?product=modelsapi\&source=docs) and export it. The free tier gives rate-limited access to `holo3-1-35b-a3b` and does not ask for a credit card.
```bash theme={null}
export HAI_API_KEY="your-api-key-here"
```
The Models API is OpenAI-compatible, so the official client works as-is, only the `base_url` changes.
```bash Python theme={null}
pip install openai
```
```bash TypeScript theme={null}
npm install openai
```
Point the client at H by overriding `base_url`, then send a request. Holo is multimodal: you can send text, images, or both. Here is a minimal text request to confirm your key and client are working.
```python Python theme={null}
import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.hcompany.ai/v1/",
api_key=os.environ.get("HAI_API_KEY"),
)
response = client.chat.completions.create(
model="holo3-1-35b-a3b",
messages=[{"role": "user", "content": "In one sentence, what is a computer-use agent?"}],
)
print(response.choices[0].message.content)
```
```typescript TypeScript theme={null}
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.hcompany.ai/v1/",
apiKey: process.env.HAI_API_KEY,
});
const response = await client.chat.completions.create({
model: "holo3-1-35b-a3b",
messages: [{ role: "user", content: "In one sentence, what is a computer-use agent?" }],
});
console.log(response.choices[0].message.content);
```
```bash cURL theme={null}
curl https://api.hcompany.ai/v1/chat/completions \
-H "Authorization: Bearer $HAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "holo3-1-35b-a3b",
"messages": [{"role": "user", "content": "In one sentence, what is a computer-use agent?"}]
}'
```
The same API and code paths work for all models; swap `model` for `holo3-122b-a10b` when you need maximum performance ([model comparison](/models)).
That is the whole setup. To use Holo on real screens, send a screenshot and continue with the agent loop or element localization below.
## Next steps
How to use Holo in your computer-use harness.
Get click coordinates from a screenshot.
IDs, limits, pricing, and lifecycle.