Skip to content
DocsHaarness Cloud
On this page

Automate with the CLI and SDKs

Use your Cloud account from a terminal, a script or a CI job. On your own computer, sign in once with haarness login. Scripts and CI use an API key instead.

Sign in from the terminal

haarness login

Your browser opens on the Haarness sign-in page. Approve the request, and the terminal says Signed in as [email protected]. Haarness Cloud is ready. The CLI and Desktop on this computer share the sign-in. Your password, passkey and two-factor codes stay in the browser; the CLI never sees them. haarness cloud login is the same command.

You want to Run
Sign in over SSH, or on a machine without a browser haarness login --device
Print the sign-in link instead of opening a browser haarness login --no-browser
See your accounts and which one is in use haarness account
Switch to another account, such as a team haarness account --select ACCOUNT_ID
See your plan and credit balances haarness account --billing
Sign out on this machine haarness logout

With --device, open the link it prints on your phone or another computer, check that the code there matches the one in the terminal, and choose Approve.

haarness account lists your accounts with their IDs and marks the one in use with *. Signing out does not stop Cloud workspaces that are running: stop them first if you want their compute to stop.

Manage Cloud workspaces from the terminal

haarness cloud workspace list
haarness cloud workspace create --title "Investigate issue"        # starts a new workspace
haarness cloud workspace create --agent AGENT_ID --title "Review"  # from a Cloud agent
haarness cloud workspace attach WORKSPACE_ID                       # open it in the terminal UI
haarness cloud workspace stop WORKSPACE_ID                         # puts it to sleep; files and chats are kept
haarness cloud workspace start WORKSPACE_ID                        # wakes it
haarness cloud agent list

Add --json to list and create for output a script can read. See Cloud workspaces for what each action does.

Start a workspace with a first message

--file takes the same JSON as the Cloud API, so a script can hand over a task in one command:

echo '{"title": "Nightly review", "initialPrompt": {"text": "Summarise the commits from yesterday."}}' > workspace.json
haarness cloud workspace create --file workspace.json --json | jq -r .workspace.id

Cloud sends the message to a new chat as soon as the workspace is ready, even if your script has already exited.

More commands

Each starts with haarness cloud.

Command Does
workspace list --agent AGENT_ID Lists only the workspaces started from that agent
workspace show WORKSPACE_ID Prints one workspace as JSON, with its state and version
workspace create --stopped Creates the workspace asleep, without starting it
workspace update WORKSPACE_ID --file FILE Changes a sleeping workspace's settings; the file includes its current version
workspace save-agent WORKSPACE_ID --name NAME --version N Saves its settings as a new Cloud agent; --include-environment copies its environment values too
agent show AGENT_ID Prints one Cloud agent as JSON
agent create --file FILE Creates a Cloud agent from a JSON file
agent update AGENT_ID --file FILE Replaces an agent's settings; the file includes its current version
agent delete AGENT_ID Deletes an agent; workspaces started from it are kept

Create an API key

Scripts, CI jobs and your own apps use an API key instead of your sign-in. You need to be an Owner or Admin of the account.

  1. In the Cloud app, open API keys under Developers and choose Create API key.

  2. Give it a Name that says where it will run, such as "GitHub Actions".

  3. Pick a Preset:

    • Models only to call models from your code;
    • Cloud automation to run workspaces, their chats and files, and schedules;
    • Models and Cloud for both;
    • Custom to tick each permission yourself.

    Permissions lists what each one allows.

  4. Choose when it Expires in: from 1 hour to 365 days, or Choose date and time. Every key expires, at most a year ahead. Shorter is safer.

  5. If the key can call models, set its Model limits: Model access (all models, only some, or none), Monthly spending cap (credits), Maximum credits per request, Requests per minute and Concurrent requests. They apply to model calls only; workspace compute uses your account's limits.

  6. Choose Create API key and copy the key straight away. It starts with hck_ and is shown only once.

To check a key, send it to /api/v1/key. The answer shows its account, permissions and expiry:

curl https://haarness.ai/api/v1/key -H "Authorization: Bearer $HAARNESS_API_KEY"

On the API keys page:

  • Edit changes a key's name, permissions, expiry and limits. The secret stays the same.
  • Rotate gives the key a new secret. The old secret stops working; permissions and spending history are kept. Rotate a key you lost: it cannot be shown again.
  • Revoke stops the key at once. This cannot be undone.

Use a key in a script or CI

Store the key in your CI system's secret store and expose it as HAARNESS_API_KEY. The haarness CLI and the SDKs read it automatically:

# GitHub Actions
jobs:
  nightly:
    runs-on: ubuntu-latest
    env:
      HAARNESS_API_KEY: ${{ secrets.HAARNESS_API_KEY }}
    steps:
      - run: curl -fsSL https://haarness.ai/install.sh | sh -s -- --no-desktop
      - run: echo "$HOME/.local/bin" >> "$GITHUB_PATH"
      - run: haarness cloud workspace list --json

While HAARNESS_API_KEY is set, the CLI uses it instead of your own sign-in, which stays saved: unset the variable to go back. A key belongs to one account, so haarness account --select cannot switch to another, and haarness logout asks you to unset the variable instead.

A revoked or expired key fails with API key invalid, expired or revoked. It never falls back to another sign-in.

Script a workspace in TypeScript or Python

The TypeScript (@haarness/sdk, Node.js 22+) and Python (haarness-sdk, Python 3.10+) SDKs are not on npm or PyPI yet. Install them from the Haarness repository. Each step below builds on the one before it.

1. Connect

HaarnessCloud talks to Haarness Cloud with the key in HAARNESS_API_KEY. Set HAARNESS_CLOUD_API_URL, or pass baseUrl (base_url in Python), to reach another Cloud, such as a self-hosted one.

import { HaarnessCloud } from "@haarness/sdk";

const cloud = new HaarnessCloud();
const { accountName } = await cloud.apiKeyContext();
console.log(accountName); // the key's account
from haarness_sdk import HaarnessCloud

cloud = HaarnessCloud()
print(cloud.api_key_context()["accountName"])  # the key's account

2. Start a workspace with a task

Give the workspace its first message when you create it. Cloud sends it to a new chat as soon as the workspace is up, even if your script has exited meanwhile, and launch follows it:

const workspace = await cloud.createWorkspace({
  title: "Nightly review",
  initialPrompt: {
    text: "Summarise yesterday's commits.",
    approvalMode: "auto",
  },
});
const chatId = workspace.created!.workspace.launch!.chatId;

let launch;
do {
  await new Promise((resolve) => setTimeout(resolve, 5000));
  launch = (await workspace.get()).launch!;
} while (launch.status === "pending" || launch.status === "running");
// "completed", or "needs_attention" when it waits for you
console.log(launch.status);
import time

workspace = cloud.create_workspace({
    "title": "Nightly review",
    "initialPrompt": {
        "text": "Summarise yesterday's commits.",
        "approvalMode": "auto",
    },
})
chat_id = workspace.created["workspace"]["launch"]["chatId"]

while True:
    time.sleep(5)
    launch = workspace.get()["launch"]
    if launch["status"] not in ("pending", "running"):
        break
print(launch["status"])  # completed, or needs_attention when it waits for you

approvalMode decides how much the chat asks: auto (the default) lets a reviewer model approve routine requests, review asks you about everything, and yolo approves everything without asking. See Review, Auto or Yolo. needs_attention means a request waits for a person: answer it in the Cloud app, or from your script as in step 4.

3. Read the answer and keep talking

const chat = workspace.chats.get(chatId);
for (const message of await chat.messages()) console.log(message.data);
const { text } = await chat.ask("Which of those touched the billing code?");
console.log(text);
chat = workspace.chats.get(chat_id)
for message in chat.messages():
    print(message["data"])
print(chat.ask("Which of those touched the billing code?")["text"])

ask waits up to five minutes for the answer. workspace.chats.create({ title }) opens another chat on the same machine, with the same files.

4. Work with its files, approvals and terminal

workspace.connect() gives you the workspace's own Haarness server: everything in the SDK guide for a local server works on it, from files to the event stream and approvals. Every write carries the file's revision: the one you read, or NEW_FILE_REVISION for a file that does not exist yet. If the file changed meanwhile, the write fails with 409 instead of overwriting someone's work.

import { NEW_FILE_REVISION } from "@haarness/sdk";

const runtime = await workspace.connect();
await runtime.writeFile({
  path: "NOTES.md",
  content: "Reviewed.\n",
  revision: NEW_FILE_REVISION,
});
const notes = await runtime.readFile({ path: "NOTES.md" });
await runtime.writeFile({
  path: "NOTES.md",
  content: notes.content + "Approved.\n",
  revision: notes.revision,
});
await runtime.setApprovalMode(chatId, { mode: "review" });
from haarness_sdk import NEW_FILE_REVISION

runtime = workspace.connect()
runtime.write_file({
    "path": "NOTES.md",
    "content": "Reviewed.\n",
    "revision": NEW_FILE_REVISION,
})
notes = runtime.read_file({"path": "NOTES.md"})
runtime.write_file({
    "path": "NOTES.md",
    "content": notes["content"] + "Approved.\n",
    "revision": notes["revision"],
})
runtime.set_approval_mode(chat_id, {"mode": "review"})

5. Let it sleep, wake it, delete it

A workspace falls asleep after its idle timeout, 10 minutes by default; its files and chats stay on its disk. Your script connects to a sleeping workspace only after waking it:

Do TypeScript Python
Wake it await workspace.start() workspace.start()
Put it to sleep now await workspace.stop() workspace.stop()
Delete it and its disk await workspace.delete() workspace.delete()
Disconnect your script await workspace.close() workspace.close()

close() only disconnects: the workspace keeps running until it falls asleep. Waking takes a moment: wait for (await workspace.get()).observedState to be running, as for a new workspace.

6. Repeat it on a schedule

A schedule posts a prompt into a chat on a cadence, waking the workspace when it sleeps:

import { randomUUID } from "node:crypto";

await cloud.createCloudSchedule(
  {
    workspaceId: workspace.id,
    chatId,
    name: "Weekday review",
    prompt: "Summarise yesterday's commits.",
    cron: "0 9 * * 1-5",
    timezone: "Europe/Paris",
  },
  { headers: { "Idempotency-Key": randomUUID() } },
);
import uuid

cloud.create_cloud_schedule(
    {
        "workspaceId": workspace.id,
        "chatId": chat_id,
        "name": "Weekday review",
        "prompt": "Summarise yesterday's commits.",
        "cron": "0 9 * * 1-5",
        "timezone": "Europe/Paris",
    },
    options={"headers": {"Idempotency-Key": str(uuid.uuid4())}},
)

The Idempotency-Key makes a retried call create one schedule, not two. previewCloudSchedule (preview_cloud_schedule) shows the next run times first, and cloudScheduleRuns lists what each run did.

7. Drive your own computers

The same client reaches a computer you connected with remote control. Pick the host, then one of its projects, and you have that project's Haarness server, as if you were on the computer:

const [host] = (await cloud.listRemoteHosts()).items;
const computer = cloud.remoteHost(host.id);
const [project] = (await computer.hostProjects()).workspaces;
const local = computer.workspace(project.id);
const thread = await local.createThread({ title: "From my script" });
console.log((await local.chat(thread.id, "Are the tests green?")).text);
host = cloud.list_remote_hosts()["items"][0]
computer = cloud.remote_host(host["id"])
project = computer.host_projects()["workspaces"][0]
local = computer.workspace(project["id"])
thread = local.create_thread({"title": "From my script"})
print(local.chat(thread["id"], "Are the tests green?")["text"])

The key needs the Read remote hosts and Control remote hosts permissions. A chat someone shared with you by link is cloud.remoteSession(id) (remote_session in Python): it reaches that chat and its subagents, and the project's files, but no other chat or the terminal. When the computer is offline the call fails with 409; when Haarness on it refuses, with 502 (bad_gateway).

The SDKs cover the whole Cloud API, including Cloud agents, API keys and billing; the repository's sdks/CLOUD-OPERATIONS.md lists every method. Signing in (passkeys included) stays in the browser: scripts use an API key.

Call models from your code

To use Cloud models from an OpenAI-compatible client, see Models and routers.

Next

Cloud API: the same tasks over HTTP, with the permissions each call needs.