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 loginYour 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 listAdd --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.idCloud 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.
In the Cloud app, open API keys under Developers and choose Create API key.
Give it a Name that says where it will run, such as "GitHub Actions".
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.
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.
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.
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 --jsonWhile 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 accountfrom haarness_sdk import HaarnessCloud
cloud = HaarnessCloud()
print(cloud.api_key_context()["accountName"]) # the key's account2. 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 youapprovalMode 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.