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

# Node

> The loop with built-in fetch. No dependencies.

Requires Node.js 18 or newer for global `fetch`.

## Polling

```javascript theme={null}
// sai.mjs
const API = process.env.SAI_API_URL ?? "https://api.simular.ai";
const HEADERS = {
  Authorization: `Bearer ${process.env.SAI_API_KEY}`,
  "Content-Type": "application/json",
};

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function api(method, path, body) {
  const res = await fetch(`${API}/v1/agents${path}`, {
    method,
    headers: HEADERS,
    body: body ? JSON.stringify(body) : undefined,
  });
  if (res.status === 429) {
    await sleep(60_000);
    return api(method, path, body);
  }
  if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
  return res.json();
}

async function run(task, machineId) {
  const { sessionId } = await api("POST", "/message", {
    message: task,
    wait: false,
    ...(machineId && { machineId }),
  });

  let cursor;
  for (;;) {
    const qs = new URLSearchParams({ sessionId, wait: "30" });
    if (cursor) qs.set("since", cursor);
    const page = await api("GET", `/events?${qs}`);
    cursor = page.cursor;

    for (const ev of page.events) {
      if (ev.type === "data-progress") console.log("  ", ev.data.text);
    }

    switch (page.status) {
      case "running":
        continue;
      case "needs_approval": {
        const a = page.approval;
        if (a.isLinkOnly) {
          console.log("Open in a browser:", a.approvalUrl);
          await sleep(15_000);
        } else if (a.approvalType === "choice") {
          const selections = a.questions.map((q) => [q.options[0].value]);
          await api("POST", "/approve", { approvalId: a.approvalId, response: "yes", selections });
        } else {
          console.log("Approving:", a.command ?? a.title);
          await api("POST", "/approve", { approvalId: a.approvalId, response: "yes" });
        }
        continue;
      }
      case "idle":
        return page.text;
      default: {
        const err = page.events.find((e) => e.type === "error");
        throw new Error(err?.errorText ?? "task failed");
      }
    }
  }
}

console.log(await run("Open Notepad, type hello, save it to the desktop as hello.txt"));
```

```bash theme={null}
SAI_API_KEY=sapi_... node sai.mjs
```

The choice handler picks the first option. Replace it with your own logic. To stop a task early, `api("POST", "/abort", { sessionId })`.

## Uploading a file

```javascript theme={null}
import { readFile } from "node:fs/promises";
import { basename } from "node:path";

async function upload(path) {
  const res = await fetch(`${API}/v1/agents/upload`, {
    method: "POST",
    headers: {
      Authorization: HEADERS.Authorization,
      "x-filename": encodeURIComponent(basename(path)),
    },
    body: await readFile(path),
  });
  if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
  return res.json();
}

const attachment = await upload("./report.pdf");
await api("POST", "/message", {
  message: "Summarise the attached PDF",
  wait: false,
  attachments: [attachment],
});
```

## Streaming

For a long-lived process, read the SSE body directly.

```javascript theme={null}
const res = await fetch(`${API}/v1/agents/message`, {
  method: "POST",
  headers: { ...HEADERS, Accept: "text/event-stream" },
  body: JSON.stringify({ message: "What is on the screen right now?" }),
});

const decoder = new TextDecoder();
let buffer = "";
let text = "";
for await (const chunk of res.body) {
  buffer += decoder.decode(chunk, { stream: true });
  let idx;
  while ((idx = buffer.indexOf("\n\n")) !== -1) {
    const frame = buffer.slice(0, idx);
    buffer = buffer.slice(idx + 2);
    const data = frame.replace(/^data: /, "");
    if (data === "[DONE]") break;
    const ev = JSON.parse(data);
    if (ev.type === "text-delta") text += ev.delta;
    if (ev.type === "data-approval-request") console.log("approval:", ev.data.approvalId);
    if (ev.type === "error") throw new Error(ev.errorText);
  }
}
console.log(text);
```

Answer approvals on the streaming path with `POST /approve` from another request; the stream stays open while it waits.
