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

# 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

```sh
haarness login
```

Your browser opens on the Haarness sign-in page. Approve the request, and the terminal says
`Signed in as you@example.com. 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

```sh
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](https://haarness.ai/docs/cloud/workspaces.md)
for what each action does.

### Start a workspace with a first message

`--file` takes the same JSON as the [Cloud API](https://haarness.ai/docs/cloud/api.md#start-a-workspace-with-a-task), so a script can
hand over a task in one command:

```sh
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](https://haarness.ai/docs/cloud/api.md#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:

```sh
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:

```yaml
# 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](https://haarness.ai/docs/server/sdks.md#install). 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.

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

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

```python
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:

```ts
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);
```

```python
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](https://haarness.ai/docs/security/permissions.md#review-auto-or-yolo-how-much-does-a-chat-ask). `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

```ts
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);
```

```python
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](https://haarness.ai/docs/server/sdks.md) 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.

```ts
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" });
```

```python
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](https://haarness.ai/docs/cloud/schedules.md) posts a prompt into a chat on a cadence, waking the workspace when it sleeps:

```ts
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() } },
);
```

```python
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](https://haarness.ai/docs/cloud/remote-hosts.md). Pick the host,
then one of its projects, and you have that project's Haarness server, as if you were on the computer:

```ts
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);
```

```python
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](https://haarness.ai/docs/cloud/api.md), 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](https://haarness.ai/docs/cloud/models-and-routers.md#call-models-from-your-own-code).

## Next

[Cloud API](https://haarness.ai/docs/cloud/api.md): the same tasks over HTTP, with the permissions each call needs.
