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

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

# Environments

> Environments are what an agent sees and acts on: a cloud browser, your own browser, or your own desktop. Pre-built environments and how to define your own.

export const CatalogGallery = ({items: itemsProp = null, kind = "agent", baseUrl = "https://agp.eu.hcompany.ai"}) => {
  const [query, setQuery] = useState("");
  const [copied, setCopied] = useState({});
  const [expanded, setExpanded] = useState({});
  const [fetched, setFetched] = useState(null);
  const [status, setStatus] = useState(itemsProp ? "ready" : "loading");
  const idOf = it => it.name || it.id || "";
  const HIDDEN = new Set(["h/holotab", "h/holo-desktop"]);
  useEffect(() => {
    if (itemsProp) return;
    const resource = ({
      agent: "agents",
      skill: "skills",
      environment: "environments"
    })[kind] || "agents";
    let cancelled = false;
    (async () => {
      setStatus("loading");
      try {
        const all = [];
        for (let page = 1; ; page += 1) {
          const resp = await fetch(`${baseUrl}/api/v2/${resource}?name=h/&size=100&page=${page}`, {
            headers: {
              Accept: "application/json"
            }
          });
          if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
          const payload = await resp.json();
          const batch = payload.items || [];
          all.push(...batch);
          const total = payload.total ?? all.length;
          if (!batch.length || all.length >= total) break;
        }
        const visible = all.filter(it => !HIDDEN.has(it.name));
        visible.sort((a, b) => (a.name || a.id || "").localeCompare(b.name || b.id || ""));
        if (!cancelled) {
          setFetched(visible);
          setStatus("ready");
        }
      } catch (e) {
        if (!cancelled) setStatus("error");
      }
    })();
    return () => {
      cancelled = true;
    };
  }, [itemsProp, kind, baseUrl]);
  const items = itemsProp || fetched || [];
  const slug = s => String(s).replace(/[^\w.-]+/g, "-");
  const crc32 = bytes => {
    let c = ~0;
    for (let i = 0; i < bytes.length; i++) {
      c ^= bytes[i];
      for (let j = 0; j < 8; j++) c = c >>> 1 ^ 0xedb88320 & -(c & 1);
    }
    return ~c >>> 0;
  };
  const downloadSkill = it => {
    const enc = new TextEncoder();
    const safe = slug(idOf(it) || "skill");
    const fm = ["---", `name: ${JSON.stringify(idOf(it))}`];
    if (it.description) fm.push(`description: ${JSON.stringify(it.description)}`);
    if (it.source) fm.push(`source: ${JSON.stringify(it.source)}`);
    if (it.url_pattern) fm.push(`url_pattern: ${JSON.stringify(it.url_pattern)}`);
    fm.push("---", "", it.body || "", "");
    const data = enc.encode(fm.join("\n"));
    const name = enc.encode(`${safe}/SKILL.md`);
    const crc = crc32(data);
    const u16 = n => new Uint8Array([n & 255, n >>> 8 & 255]);
    const u32 = n => new Uint8Array([n & 255, n >>> 8 & 255, n >>> 16 & 255, n >>> 24 & 255]);
    const local = [u32(0x04034b50), u16(20), u16(0), u16(0), u16(0), u16(0), u32(crc), u32(data.length), u32(data.length), u16(name.length), u16(0), name, data];
    const localSize = local.reduce((s, c) => s + c.length, 0);
    const central = [u32(0x02014b50), u16(20), u16(20), u16(0), u16(0), u16(0), u16(0), u32(crc), u32(data.length), u32(data.length), u16(name.length), u16(0), u16(0), u16(0), u16(0), u32(0), u32(0), name];
    const centralSize = central.reduce((s, c) => s + c.length, 0);
    const eocd = [u32(0x06054b50), u16(0), u16(0), u16(1), u16(1), u32(centralSize), u32(localSize), u16(0)];
    const parts = [...local, ...central, ...eocd];
    const out = new Uint8Array(parts.reduce((s, c) => s + c.length, 0));
    let off = 0;
    for (const c of parts) {
      out.set(c, off);
      off += c.length;
    }
    const url = URL.createObjectURL(new Blob([out], {
      type: "application/zip"
    }));
    const a = document.createElement("a");
    a.href = url;
    a.download = `${safe}.zip`;
    document.body.appendChild(a);
    a.click();
    a.remove();
    setTimeout(() => URL.revokeObjectURL(url), 1000);
  };
  const copy = (key, text) => {
    try {
      navigator.clipboard.writeText(text);
    } catch (e) {}
    setCopied(s => ({
      ...s,
      [key]: true
    }));
    setTimeout(() => setCopied(s => ({
      ...s,
      [key]: false
    })), 1500);
  };
  const copyIcon = on => on ? <svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
        <path d="M20 6L9 17l-5-5" />
      </svg> : <svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
        <rect x="9" y="9" width="12" height="12" rx="2" />
        <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
      </svg>;
  const CopyChip = ({k, text, label}) => <button type="button" onClick={() => copy(k, text)} aria-label={label} title={label} className={`${copied[k] ? "shrink-0 rounded-md p-1.5 transition text-emerald-600 dark:text-emerald-400" : "shrink-0 rounded-md p-1.5 transition text-zinc-400 hover:bg-zinc-100 hover:text-zinc-900 dark:hover:bg-zinc-800 dark:hover:text-zinc-100"}`}>
      {copyIcon(copied[k])}
    </button>;
  const ExpandChip = ({k}) => <button type="button" onClick={() => setExpanded(s => ({
    ...s,
    [k]: !s[k]
  }))} aria-label={expanded[k] ? "Hide body" : "Show body"} title={expanded[k] ? "Hide body" : "Show body"} className="shrink-0 rounded-md p-1.5 text-zinc-400 transition hover:bg-zinc-100 hover:text-zinc-900 dark:hover:bg-zinc-800 dark:hover:text-zinc-100">
      <svg className={`${expanded[k] ? "h-4 w-4 rotate-180 transition-transform" : "h-4 w-4 transition-transform"}`} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
        <path d="M6 9l6 6 6-6" />
      </svg>
    </button>;
  const DownloadChip = ({it}) => <button type="button" onClick={() => downloadSkill(it)} aria-label="Download SKILL.md" title="Download SKILL.md" className="shrink-0 rounded-md p-1.5 text-zinc-400 transition hover:bg-zinc-100 hover:text-zinc-900 dark:hover:bg-zinc-800 dark:hover:text-zinc-100">
      <svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
        <path d="M12 3v12" />
        <path d="M7 12l5 5 5-5" />
        <path d="M5 21h14" />
      </svg>
    </button>;
  const glyphs = {
    environment: <svg className="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
        <rect x="3" y="4" width="18" height="16" rx="1.5" />
        <path d="M3 9h18" />
        <path d="M7 6.5h.01M10 6.5h.01" />
      </svg>
  };
  const envKindGlyphs = {
    web: <svg className="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
        <circle cx="12" cy="12" r="9" />
        <path d="M3 12h18" />
        <path d="M12 3c2.5 2.6 3.8 5.7 3.8 9s-1.3 6.4-3.8 9c-2.5-2.6-3.8-5.7-3.8-9s1.3-6.4 3.8-9z" />
      </svg>,
    desktop: <svg className="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
        <rect x="3" y="4" width="18" height="12" rx="1.5" />
        <path d="M8 20h8" />
        <path d="M12 16v4" />
      </svg>,
    code: <svg className="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
        <rect x="3" y="4" width="18" height="16" rx="1.5" />
        <path d="M7.5 9l3 3-3 3" />
        <path d="M13 15h4" />
      </svg>,
    mcp: <svg className="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
        <path d="M9 2v6" />
        <path d="M15 2v6" />
        <path d="M6 8h12v2.5a6 6 0 0 1-12 0z" />
        <path d="M12 16.5V22" />
      </svg>,
    memory: <svg className="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
        <ellipse cx="12" cy="5" rx="8" ry="3" />
        <path d="M4 5v6c0 1.7 3.6 3 8 3s8-1.3 8-3V5" />
        <path d="M4 11v6c0 1.7 3.6 3 8 3s8-1.3 8-3v-6" />
      </svg>
  };
  const ClampedText = ({text, lines = 5}) => {
    const ref = useRef(null);
    const [open, setOpen] = useState(false);
    const [overflows, setOverflows] = useState(false);
    useEffect(() => {
      const el = ref.current;
      if (el) setOverflows(el.scrollHeight - el.clientHeight > 1);
    }, [text]);
    return <div className="min-w-0">
        <div ref={ref} className="whitespace-pre-wrap break-words" style={open ? undefined : {
      display: "-webkit-box",
      WebkitLineClamp: lines,
      WebkitBoxOrient: "vertical",
      overflow: "hidden"
    }}>
          {text}
        </div>
        {overflows || open ? <button type="button" onClick={() => setOpen(o => !o)} className="mt-1 text-xs font-medium text-zinc-500 transition hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100">
            {open ? "Show less" : "Show more"}
          </button> : null}
      </div>;
  };
  const KINDS = {
    agent: {
      noun: "agents",
      rows: it => {
        const out = [["model", it.model || "default"]];
        if (Array.isArray(it.environments) && it.environments.length) out.push(["environment", it.environments.map(e => typeof e === "string" ? e : e.id || e.kind || "inline").join(", ")]);
        if (it.instructions) out.push(["instructions", <ClampedText text={it.instructions} />]);
        if (Array.isArray(it.subagents) && it.subagents.length) out.push(["subagents", it.subagents.map(s => typeof s === "string" ? s : s.name || "").join(", ")]);
        if (Array.isArray(it.skills) && it.skills.length) out.push(["skills", <div className="flex flex-wrap gap-1.5">
              {it.skills.map(s => <a key={s} href={`/agents-api/skills/overview#${slug(s)}`} className="inline-flex items-center rounded-md bg-zinc-100 px-2 py-0.5 font-mono text-xs text-zinc-700 no-underline transition hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-300 dark:hover:bg-zinc-700">
                  {s}
                </a>)}
            </div>]);
        return out;
      }
    },
    environment: {
      noun: "environments",
      rows: it => {
        const out = [["kind", it.kind || "?"]];
        const mode = it.mode;
        if (mode && typeof mode === "object") {
          out.push(["mode", mode.type || "?"]);
          if (mode.type === "visual") {
            if (mode.width && mode.height) out.push(["viewport", `${mode.width} x ${mode.height}`]);
            if (mode.markdown) out.push(["markdown", "on"]);
          } else if (mode.type === "text" && mode.chunk_size) {
            out.push(["chunk size", `${mode.chunk_size} chars`]);
          }
        } else if (typeof mode === "string") {
          out.push(["mode", mode]);
          if (it.width && it.height) out.push(["viewport", `${it.width} x ${it.height}`]);
        }
        if (it.start_url) out.push(["start url", it.start_url.replace(/^https?:\/\//, "")]);
        if (it.network && it.network.proxy_url) out.push(["proxy", "configured"]);
        if (it.instructions) out.push(["instructions", <ClampedText text={it.instructions} />]);
        return out;
      }
    },
    skill: {
      noun: "skills",
      rows: it => {
        const out = [];
        if (it.source) out.push(["source", it.source]);
        if (it.url_pattern) out.push(["url pattern", it.url_pattern]);
        return out;
      },
      headerAction: it => <div className="-mr-1 flex shrink-0 items-center gap-0.5">
          <DownloadChip it={it} />
          {it.body ? <ExpandChip k={`${idOf(it)}:body`} /> : null}
        </div>,
      extra: it => it.body && expanded[`${idOf(it)}:body`] ? <div className="mt-5 border-t border-zinc-100 pt-4 dark:border-zinc-800/70">
            <div className="whitespace-pre-wrap text-sm leading-relaxed text-zinc-700 dark:text-zinc-300">
{it.body}
            </div>
          </div> : null
    }
  };
  const cfg = KINDS[kind] || KINDS.agent;
  const q = query.trim().toLowerCase();
  const matches = items.filter(it => !q ? true : [idOf(it), it.description, it.model, it.kind, it.start_url].filter(Boolean).join(" ").toLowerCase().includes(q));
  return <div className="not-prose">
      <div className="relative mb-6 w-72 max-w-full">
        <svg className="pointer-events-none absolute inset-y-0 left-0 my-auto h-4 w-4 text-zinc-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
          <circle cx="11" cy="11" r="7" />
          <path d="M21 21l-4.3-4.3" />
        </svg>
        <input type="text" value={query} onChange={e => setQuery(e.target.value)} placeholder={`Search ${matches.length} ${matches.length === 1 ? cfg.noun.replace(/s$/, "") : cfg.noun}`} className="w-full border-0 border-b border-zinc-200 bg-transparent py-2 pl-7 pr-3 text-sm text-zinc-900 placeholder-zinc-400 transition focus:border-zinc-900 focus:outline-none focus:ring-0 dark:border-zinc-800 dark:text-zinc-100 dark:placeholder-zinc-500 dark:focus:border-zinc-100" />
      </div>

      {status === "loading" ? <div className="py-12 text-center text-sm text-zinc-400 dark:text-zinc-500">Loading {cfg.noun}.</div> : status === "error" ? <div className="py-12 text-center text-sm text-zinc-400 dark:text-zinc-500">
          Could not load {cfg.noun} right now. Please try again later.
        </div> : matches.length === 0 ? <div className="py-12 text-center text-sm text-zinc-400 dark:text-zinc-500">
          {items.length === 0 ? `No built-in ${cfg.noun} are published yet. This list updates automatically as they ship.` : `No ${cfg.noun} match "${query}".`}
        </div> : <div className="space-y-3">
          {matches.map(it => {
    const id = idOf(it);
    const nameKey = `${id}:name`;
    const list = cfg.rows(it);
    const icon = kind === "environment" ? envKindGlyphs[it.kind] || glyphs.environment : glyphs[kind];
    return <div key={id} id={slug(id)} className="min-w-0 scroll-mt-24 rounded-xl border border-zinc-200 bg-white p-6 dark:border-zinc-800 dark:bg-zinc-950">
                <div className="flex items-start justify-between gap-4">
                  <div className="flex min-w-0 items-center gap-3">
                    {icon ? <span className="shrink-0 text-zinc-900 dark:text-zinc-100">{icon}</span> : null}
                    <button type="button" onClick={() => copy(nameKey, id)} title="Copy name" className="group flex min-w-0 items-center gap-1.5 text-left">
                      <span className="min-w-0 break-words font-mono text-[15px] font-bold tracking-tight text-zinc-900 dark:text-zinc-100">{id}</span>
                      <span className={`${copied[nameKey] ? "shrink-0 transition text-emerald-600 dark:text-emerald-400" : "shrink-0 transition text-zinc-300 opacity-0 group-hover:opacity-100 dark:text-zinc-600"}`}>
                        {copyIcon(copied[nameKey])}
                      </span>
                    </button>
                  </div>
                  {cfg.headerAction ? cfg.headerAction(it) : <div className="-mr-1 shrink-0"><CopyChip k={id} text={JSON.stringify(it, null, 2)} label="Copy JSON" /></div>}
                </div>

                {it.description ? <p className="mt-3 max-w-2xl text-sm leading-relaxed text-zinc-600 dark:text-zinc-400">{it.description}</p> : null}

                {list.length ? <dl className="mt-5 space-y-2">
                    {list.map(([k, v]) => <div key={k} className="flex gap-4 text-sm">
                        <dt className="w-28 shrink-0 text-xs uppercase tracking-wider text-zinc-400 dark:text-zinc-500">{k}</dt>
                        <dd className={`${k === "instructions" || typeof v !== "string" ? "min-w-0 break-words text-zinc-700 dark:text-zinc-300" : "min-w-0 break-words font-mono text-zinc-700 dark:text-zinc-300"}`}>{v}</dd>
                      </div>)}
                  </dl> : null}

                {cfg.extra ? cfg.extra(it) : null}
              </div>;
  })}
        </div>}
    </div>;
};

An environment is what the agent sees and acts on, one step at a time: look at the screen, reason, act.

An agent lists its environments in `environments`, each a catalog id or an inline object. The field is required, with at most one environment per kind. It may be empty (`[]`) only for a pure [manager](/agents-api/multi-agent) with `subagents` or an agent with [custom tools](/agents-api/custom-tools).

| Environment | `kind` | `host` | Runs on |
| - | - | - | - |
| [Browser](/agents-api/browser/configuration) | `web` | `cloud` (default) | A browser H hosts per session. |
| Local browser | `web` | `user_device` | Chrome on your machine, via [local control](/agents-api/local-control). |
| Local desktop | `desktop` | `user_device` | Your machine's real screen, mouse, and keyboard, via [local control](/agents-api/local-control). |

Local control keeps the agent in H's cloud and only the browser or desktop on your machine. To run the agent itself on your machine, see [Local mode](/agents-api/local-mode).

## Toolbox

Each environment gives the agent a fixed set of actions plus built-in guidance on using them. The browser, for example, adds navigate, click, type, scroll, and read. You cannot add or remove actions. To shape how the agent uses them, set the agent's [`instructions`](/agents-api/agents/overview#configure-an-agent) or attach [skills](/agents-api/skills/overview).

## Built-in environments

H ships browsers in the `h/` catalog. List them with [`GET /api/v2/environments`](/agents-api/environments/list).

<CatalogGallery kind="environment" />

## Create your own

Most agents define their environment inline. Create a catalog environment when several agents share it, then reference it by `id`:

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

Then use it in an agent: `"environments": ["wide-browser"]`. All browser fields: [Browser configuration](/agents-api/browser/configuration).

To make an agent faster, see [Optimize for speed](/agents-api/optimize-for-speed).


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