# Bookmarks

> Import a browser bookmark export once and give many profiles the same bookmarks — merged or replaced, in one folder or everywhere — applied by each profile's own browser.

A bookmark import is a list of links read from a browser's bookmark export. You import the file once into the workspace library, then queue it on any number of profiles. Each profile's browser applies it through its own bookmarks manager at the **next start**, before the session is reported ready, and reports exactly what it did.

## Import a file

Export bookmarks from any browser (Chrome, Edge, Brave, Firefox …: *Bookmark manager → Export bookmarks*). The result is a `bookmarks.html` file in the Netscape bookmark format. The Browlix server reads it with a real HTML parser and keeps only what is safe:

- only `http://` and `https://` links — bookmarklets (`javascript:`), `data:`, `file:`, `chrome:`, `place:` and every other scheme are **rejected with a reason**;
- addresses carrying a user name or password (`https://user:pass@…`) are refused and never stored;
- titles are plain text; icons and descriptions in the file are ignored;
- the bookmarks bar stays the bookmarks bar, everything else becomes *other bookmarks*, folders are kept (up to 10 levels);
- at most 10 MB and 5,000 links per import; the same link twice in the same folder is kept once.

```ts tab=TypeScript
import { readFile } from "node:fs/promises";

const imported = await bx.call("bookmarks.import", {
  body: { name: "Team links October", html: await readFile("./bookmarks.html", "utf8"), file_name: "bookmarks.html" },
});
const set = imported.data.set;
console.log(set.links, set.rejected_count); // what was kept, how many were refused
```

```json tab=MCP
{
  "name": "bookmarks_import",
  "arguments": { "name": "Team links October", "html": "<!DOCTYPE NETSCAPE-Bookmark-file-1>…" }
}
```

Importing the exact same list again returns the existing import (`created: false`). `bookmarks.get` returns every link (`root` bar/other, folder `path`, `title`, `url`) and up to 50 examples of rejected links with their reason: `unsafe_scheme`, `invalid_url`, `credentials`, `too_long`, `too_deep` or `over_limit`.

## Apply it to profiles

Applying is **compare-and-set per profile**: first read each profile's bookmark revision, then send it back as `expected_rev`. A profile whose bookmarks changed in between (another import was queued or applied) is reported as `conflict` and nothing is queued for it — read again and decide.

```ts tab=TypeScript
const set = (await bx.call("bookmarks.list", {})).data.find((s) => s.name === "Team links October");
const profileIds = ["…", "…"];
const preview = await bx.call("bookmarks.preview", { body: { profile_ids: profileIds } });

if (set) {
  const queued = await bx.call("bookmarks.apply", {
    path: { id: set.id },
    body: {
      mode: "merge",
      scope: "folder",
      folder: "Team links",
      targets: preview.data.map((p) => ({ profile_id: p.profile_id, expected_rev: p.rev })),
    },
  });
  console.log(queued.data.queued, queued.data.conflicts, queued.data.not_found);
}
```

| Choice | Effect in each profile |
|---|---|
| `mode: "merge"` | adds links that are not already in the target folder; existing bookmarks stay |
| `mode: "replace"` | first removes the bookmarks in the target, then imports the list |
| `scope: "folder"` | everything goes into one folder on the bookmarks bar (`folder`, created if missing), keeping the file's subfolders |
| `scope: "all"` | the bookmarks bar and other bookmarks exactly as in the file |

`replace` with `scope: "all"` removes **every** existing bookmark of those profiles first — the web app asks for an explicit confirmation before sending it.

Up to 500 profiles per call; profiles outside your groups are `not_found`. A queued application can be withdrawn with `bookmarks.cancel` until a browser takes it.

## What the machine does

A machine needs worker **0.16.0** or newer. An older worker never receives imports: they stay queued (the session shows them as waiting) until the profile opens on an updated machine.

When a profile starts, the session start names each queued import and how many links it carries; only the machine holding that start can fetch the links. Its browser opens its own bookmarks manager in the background, applies the imports in the order they were queued (up to 8 per start), closes it, and only then reports the session ready. `profiles.bookmarks` and `sessions.get` show the browser's own counts:

| State | Meaning |
|---|---|
| `pending` | waiting for the profile's next start |
| `applying` | a browser took it and is applying it |
| `applied` | done: `added`, `duplicates` (already there) and `removed` (by replace) |
| `failed` | the browser could not apply it; the reason says why (changes already made are counted) |
| `cancelled` | withdrawn before a browser took it |

An import that a machine took but did not confirm — the start failed, or the machine stopped before reporting — goes back to `pending` and runs at the next start. Merging is idempotent: running it again adds nothing that is already there.

## Errors you will see

| Code | When |
|---|---|
| `FORBIDDEN` | importing, applying, withdrawing or deleting without `bookmark.import` |
| `VALIDATION_FAILED` | not a bookmark export, over 10 MB, no http(s) link left, a missing folder name with `scope: "folder"`, or more than 500 profiles |
| `CONFLICT` | withdrawing an import a browser is applying (or that already ended), or deleting an import profiles still wait for |
| `NOT_FOUND` | the import does not exist in this workspace |

Every decision is in the audit log: `bookmark.import`, `bookmark.apply` (per profile, with mode, scope and revision), `bookmark.cancel`, `bookmark.delete`.
