- Brand
- Anchor
- Category
- Productivity
- Primary Subcategory
- Cloud File Storage
Integration details
Description
Anchor gives ChatGPT a shared drive where it can create files and organize them in folders. ChatGPT can read and write files in HTML, Markdown (MD), JSON, CSV, and other text formats, design typed tables, and query them with SQL. It can move files around, make copies, and keep everything organized. Every file, table, and folder it creates is shareable with your team, while Anchor handles storage, permissions, and versioning. Log in once, with no API keys and no setup, and ChatGPT only ever touches what you can access.
- Integration type
- Plugin
- Verification status
- Not applicable
- Platform
- ChatGPT
- Primary Subcategory
- Cloud File Storage
- Secondary Subcategories
- None listed
- Brand
- Anchor
- Access
- Account required
- First tracked
- 2026-08-25
- Tool count
- 18
- Market
- US
The Primary Subcategory used for this profile’s headline score.
Other Subcategories where the Integration is listed.
Agent DiscoverabilityChatGPT Discoverability ScoreUpdated dailyThe first to track the ChatGPT, Claude, Muse, Grok & Google ecosystems, with the largest database on the core platforms. Tracked daily since .
0/100
Invisible#3of 17
in Cloud File Storage- Picked
- 0.0/100
- Found
- 0.0/100
- Positioned
- –/100
Category leaderboard
Cloud File Storage
- 1D
Dropbox60
- 2B
Box53
- 3A
Anchor0
- 4C
Context — ctxt.io0
- 5D
DocSend0
Get alerts for Anchor
Get updates when Anchor’s Discoverability Score or category rank changes.
Competing in ChatGPT Cloud File Storage
View Category18 tools agents can invoke
Add a new column to an existing Anchor table. This is the column-level counterpart to create_table — same column schema, but for a single column added after the table already exists. Example input (project_id holds a record_id from a Projects table, linked but not enforced): { "table_id": "a195e7a1-b0a0-7f00-a1b2-c3d4e5f60001", "column": { "identifier": "project_id", "display_name": "Project", "type": "INTEGER", "isUnique": false, "isRequired": false, "linkedTableId": "a195e7a1-b0a0-7f00-a1b2-c3d4e5f60002" } } Example with required ENUM column and defaultValue: { "table_id": "a195e7a1-b0a0-7f00-a1b2-c3d4e5f60001", "column": { "identifier": "priority", "display_name": "Priority", "type": "ENUM", "isUnique": false, "isRequired": true, "possibleValues": ["P0", "P1", "P2"], "defaultValue": "P2" } } Under the hood this runs an ALTER TABLE … ADD COLUMN on the underlying PostgreSQL table: ALTER TABLE anchor_tables."<table_uuid>" ADD COLUMN <identifier> <PG_TYPE> [NOT NULL] [UNIQUE] [DEFAULT <value>::<PG_TYPE>] [CHECK (...)]; Type mapping: STRING → TEXT, NUMBER → DOUBLE PRECISION, INTEGER → BIGINT, BOOLEAN → BOOLEAN, ENUM → TEXT (with CHECK constraint), DATE → DATE, DATETIME → TIMESTAMPTZ, EMAIL / CREATED_BY_EMAIL → TEXT. RULES: - The column uses exactly one of two forms: standard, or ENUM (requires possibleValues). - Cannot add a column whose identifier already exists in the table. - If the column is required (isRequired: true), defaultValue MUST be set — existing rows are backfilled with it via DEFAULT. The column will be rejected without it. - If the column is not required, defaultValue is optional — existing rows get NULL when omitted. - Unique columns: when isUnique is true, isRequired must be false and defaultValue must not be set — existing rows are filled with NULL (which satisfies uniqueness). - CREATED_BY_EMAIL columns: defaultValue must NOT be set (the enforcement trigger overrides it on every INSERT/UPDATE with the caller's email). New rows inserted after the column is added are always populated by the trigger. Existing rows cannot be backfilled — the trigger only fires on INSERT/UPDATE — so isRequired may only be true if the table currently has zero rows. On a non-empty table, isRequired must be false; existing rows will be NULL while new rows are auto-filled by the trigger.
add_table_column
Builds an Anchor app from the contents of a temporary folder. Two modes: NEW APP — pass temporary_folder_id + name + destination_folder_id. Creates a new APP file in destination_folder_id, which must be in the temporary folder's org, and builds+publishes v1. NEW VERSION — pass temporary_folder_id + app_id. Builds a new version of app_id from scratch using every file currently in the temporary folder — nothing carries over from the previous version except what you put there yourself (e.g. via unpack_file first). Auto-publishes iff the app isn't shared with anyone else; otherwise the owner must publish it from the app's management page. Required temporary-folder files: index.tsx, favicon.json. Optional: index.css, functions.json, plus any number of helper modules index.tsx imports. Reserved build-output names (bundle.html, fetch.ts) are rejected if present — those are emitted by the build pipeline, not authored by you. See write_file's description for exactly how to author index.tsx, functions.json, and favicon.json (including the JSON-escaping rules for functions.json's SQL strings). index.tsx is bundled, NOT typechecked. The ./fetch helpers take positional arguments in inputSchema order — addTask(title) not addTask({ title }), listTasks() not listTasks({}). An object argument compiles, publishes, and then fails on every call with INVALID_REQUEST, so re-read every ./fetch call in index.tsx before you build. Full contract: write_file's description. index.tsx is the bundle's entry module and is responsible for mounting. The served page contains an empty <div id="root"></div> and runs your compiled bundle as a module script; the platform adds no bootstrapping code. End index.tsx with createRoot(document.getElementById('root')!).render(<App />), importing createRoot from 'react-dom/client'. A file that only exports a component produces a blank page, and the build rejects it with NO_ROOT_MOUNT. functions.json declares this version's complete data-access footprint via dependencies (TABLE and OTHER entries) plus the SQL functions that actually touch them. The build verifies the declaration is exactly right, in both directions, before it will succeed: - Every table or OTHER file a SQL function touches must be declared in dependencies, at the level actually used (READ vs WRITE) — an undeclared or under-declared touch fails with UNDECLARED_TABLE_ACCESS. - Every declared TABLE entry must actually be used at that level by some function — an unused or over-declared entry fails with UNUSED_TABLE_DEPENDENCY. An app can never ask for more access than it uses. - Every declared entry requires YOUR OWN direct access at that level before the build can succeed (TABLE READ needs your direct CONSUMER access, TABLE WRITE needs your direct CONTRIBUTOR access, OTHER needs your direct CONSUMER access) — building never grants access on your behalf. - Every OTHER entry must exist, be an OTHER file (never APP or TABLE), and not sit inside any temporary folder. - An image the app displays is such an OTHER entry: declare the file (no functions entry needed — the unused-dependency check is TABLE-only) and render it with anchorOtherFileFetch(fileId) from ./fetch as the <img src>. Never base64-encode an image into index.tsx. Building never grants anyone access to anything — it only records the requirement. If the app declares dependencies, it is unusable by everyone right after the build, including you, until each person separately consents to what it does with their access (and, for anyone lacking the access itself, requests it from the owner first); direct the user to the app's management page (app_management_link) for that flow. An app that declares no dependencies has nothing to consent to, and is reachable as soon as someone has access to it. On a successful build, the temporary folder (and everything in it) is deleted — do not try to write_file/cp/mv into temporary_folder_id afterward, it no longer exists. A failed build leaves the temporary folder in place so you can fix the problem and retry without re-writing every file. There is no other cleanup path for a temporary folder: one that's never successfully built against sits there permanently.
build_app
Turn a temporary folder of files into an Anchor document, or into a new version of one that already exists. An Anchor document is one file made of several: one HTML file per page, a config.json listing them, and whatever CSS and images they use. There is no build step — the files are served as written. To make one: mkdir(temporary: true), write the files into that folder with write_file, copy in any images with cp, then call this — naming a normal folder in the staging folder's org as the destination, never the staging one. To change an existing one: unpack_file first, edit the files it writes out, then call this with that folder and the base_version unpack_file returned. To restore an older version, unpack_file it by number and call this with the folder unchanged, the same way: it becomes a new version with the old content. Everything validates before anything is stored, so a rejected commit creates no version and leaves the folder untouched for you to fix and retry. A successful one deletes the folder. Write ordinary HTML. One complete HTML document per page, no framework. Anchor supplies everything around the page: page navigation, comments, version history, printing. Do not build navigation, in-page tabs, or anything that hides content behind a click — <details> included. What you write is also what gets printed and thumbnailed. Let height follow content, and never hide anything behind scrolling. Put the content in the markup — search engines, text extraction and version diffs see only the DOM. JavaScript is for interaction, never for producing text. Every element inside <body> needs an id, unique across every file in the document. This is checked and it is literal — a layout wrapper needs one exactly as much as a paragraph does. Inside an <svg>, only the <svg> itself and the tags that carry words (<text>, <tspan>, <textPath>, <title>, <desc>); shapes and plumbing do not. Keep ids the same across edits wherever the content has not changed: comments anchor to them, and a changed id is a lost comment. Renaming an id while its element stays is the easy mistake — it feels like tidying, and to a comment it is a deletion. If you restructure, keep the id on any element whose words survive, even where it has moved or been re-tagged. Committing names every id that is no longer present, so check each one and put back any that should have survived. Wrap each sentence in a <span>, so a comment has a natural unit to attach to. No browser storage — no localStorage, no cookies. The page has no internet. Everything it loads is a file inside the document, by relative path — Google Fonts is the only exception, and data: URIs are refused. fetch, XHR and WebSockets fail too. Links are the one way out: they may point anywhere, and Anchor handles the click, so target and window.open do nothing. Tags, attributes and ARIA roles are allowlisted, and narrower than you would guess: no form controls, iframes, canvas or on* handlers, and <label>, <progress> and interactive roles are out too — except the few below that a reader's state lives on, which are the only controls a document has, whether or not it keeps what the reader does. A rejection lists everything that would have been accepted. Ordinary CSS is fine, inline in a <style> block or in a .css file beside the page. Charts and diagrams are SVG, which stays readable to search engines, diffs and printers. config.json says what the pages are, in the order they appear: { "kind": "document", "tabs": [ { "id": "k3f9x2", "title": "Introduction", "file": "page-1.html" }, { "id": "m7p2qd", "title": "How it works", "file": "page-2.html" } ] } Each entry: `id` is a stable handle, `title` is what Anchor's chrome shows, `file` is a .html file at the root of the folder. **An `id` is opaque and permanent.** Invent about six characters with no relation to the title — a letter, then letters or digits, like `k3f9x2` — and never change it again. It is what a link to this page points at and what a reader's saved state is filed under, so changing one silently breaks both: a link that used to open here lands on the first page instead. Retitling and reordering are free, and neither touches the `id`. Do not make it resemble the title, or it will look wrong the moment the title changes and invite exactly that edit. Pick a new `id` only for a page that did not exist before. A document can show live rows from a table. Declare them in two more halves of config.json, beside `tabs` — the same `dependencies` and `functions` an app's functions.json uses, minus its `version` — and leave both out when there is no data to read: { "dependencies": [ { "id": "targets", "type": "TABLE", "file_id": "<the table's file id>", "access": "READ", "description": "Reads quarterly targets for the table on page 1." } ], "functions": [ { "type": "SQL", "name": "quarterlyTargets", "description": "Every target, by quarter", "sql": "SELECT quarter, target, actual FROM \"<the table's file id>\" ORDER BY quarter", "inputSchema": [], "outputSchema": { "quarter": { "type": "string", "description": "Quarter, as 2026-Q1" }, "target": { "type": "number", "description": "Target, in USD" }, "actual": { "type": "number", "description": "Booked so far, in USD" } } } ] } A dependency's `id` is a short lowercase name of your choosing, unique in the file. Its `description` is what the table's owner reads when they decide whether to allow this, so write it for them. Every function is `"type": "SQL"`, and `inputSchema` must be empty: nobody is there to supply an argument, and a query with parameters would return nothing. Every query runs as written, for each reader, with only that reader's own access — a reader who cannot see the table sees no rows, and the document cannot lend them any. The rows are handed to the page. There is nothing to call and no endpoint to reach: each query's rows arrive as an `anchor:data` event, and the latest rows for each name stay readable at `window.anchorData(name)`, so a script that starts listening late has missed nothing. Do both, or the page is blank whenever delivery wins the race: <script id="targets-draw"> function draw(rows) { /* replace the contents of one container */ } window.addEventListener('anchor:data', function (event) { if (event.detail.name === 'quarterlyTargets') draw(event.detail.rows); }); var rows = window.anchorData && window.anchorData('quarterlyTargets'); if (rows) draw(rows); </script> The event fires again whenever the table changes, so draw by replacing what you drew, never by appending. Rows drawn this way are not in the markup, which means they are not in a print, a thumbnail or a search result — so the document's own words still belong in the DOM, and the container they land in needs markup that reads sensibly while empty. Ids are checked on what you commit, so rows built by script need none. A reader can interact with a document, and you decide whether what they do is kept. Both kinds are written the same way — there are no form controls, so anything interactive is an element with an ARIA role and a listener — which is exactly why the choice has to be made on purpose. **Kept, and shared with everyone.** `anchor.set(id, attribute, value)` sets the attribute and stores it as a new version of the document: every reader sees it, it survives a reload, and it can be undone. This is what a shared checklist is — somebody ticks `Tests written` and their colleagues open the document and find it ticked. <span id="tests-done" role="checkbox" aria-checked="false" tabindex="0">Tests written</span> <script id="tests-done-script"> var box = document.getElementById('tests-done'); box.addEventListener('click', function () { var next = box.getAttribute('aria-checked') === 'true' ? 'false' : 'true'; anchor.set('tests-done', 'aria-checked', next); // stored, and everyone sees it }); </script> **Not kept, and nobody else's business.** Change the page yourself and the change lives in that one reader's browser until they reload. That is the right answer for anything only the person reading it should see: a mock-up of a feature inside a spec, a worked example somebody steps through. Nobody wants a colleague's click on a mock saved into the document as a new version. <div id="demo-card" class="card">Your first project</div> <span id="demo-dark" role="switch" aria-checked="false" tabindex="0">Preview in dark mode</span> <script id="demo-dark-script"> var demo = document.getElementById('demo-dark'); demo.addEventListener('click', function () { var on = demo.getAttribute('aria-checked') !== 'true'; demo.setAttribute('aria-checked', String(on)); // this reader only, gone on reload document.getElementById('demo-card').classList.toggle('dark', on); }); </script> **They look the same until somebody reloads, so ask one question:** should a second reader see this change? If yes it goes through `anchor.set`; if no it does not. A shared checklist built without it works perfectly for the person who ticked it and is empty for everyone else, and neither of them will find out until one of them reopens the document. Stored state is read back out of the markup, where it is — not out of a variable you kept alongside it. What can be stored is a state, on an element whose role owns it: `aria-checked` on a checkbox, option, radio or switch; `aria-expanded` on a checkbox, columnheader, listbox, row or rowheader; `aria-selected` on a columnheader, option, row or rowheader; `aria-valuenow` on a slider, within the `aria-valuemin` and `aria-valuemax` you gave it. Anything else is refused and the reason is logged in the page's console. Write the starting value into the markup at commit. It is the state, so a document that has never been opened already reads correctly — and it is what the print, the thumbnail and a search engine see. Saving needs edit access. A reader with view access sees the control and the change is put back, which is a good reason not to build one that looks disabled when it is not. A document reflows to whatever width it is read at. Use a wide viewport well — especially for tables — and keep running text at a readable measure rather than letting it stretch edge to edge. The page itself must never scroll sideways, down to a phone's width. Anything that cannot narrow — a wide table, a long code line, a diagram — scrolls inside its own container instead, and that is the one place a scrolling region belongs.
commit_document
Turn a temporary folder of files into an Anchor presentation, or into a new version of one that already exists. An Anchor presentation is one file made of several: one HTML file per slide, a config.json listing them, and whatever CSS and images they use. There is no build step — the files are served as written. To make one: mkdir(temporary: true), write the files into that folder with write_file, copy in any images with cp, then call this — naming a normal folder in the staging folder's org as the destination, never the staging one. To change an existing one: unpack_file first, edit the files it writes out, then call this with that folder and the base_version unpack_file returned. To restore an older version, unpack_file it by number and call this with the folder unchanged, the same way: it becomes a new version with the old content. Everything validates before anything is stored, so a rejected commit creates no version and leaves the folder untouched for you to fix and retry. A successful one deletes the folder. Write ordinary HTML. One complete HTML document per slide, no framework. Anchor supplies everything around the page: slide navigation, comments, version history, printing. Do not build navigation, in-page tabs, or anything that hides content behind a click — <details> included. What you write is also what gets printed and thumbnailed. Let height follow content, and never hide anything behind scrolling. Put the content in the markup — search engines, text extraction and version diffs see only the DOM. JavaScript is for interaction, never for producing text. Every element inside <body> needs an id, unique across every file in the presentation. This is checked and it is literal — a layout wrapper needs one exactly as much as a paragraph does. Inside an <svg>, only the <svg> itself and the tags that carry words (<text>, <tspan>, <textPath>, <title>, <desc>); shapes and plumbing do not. Keep ids the same across edits wherever the content has not changed: comments anchor to them, and a changed id is a lost comment. Renaming an id while its element stays is the easy mistake — it feels like tidying, and to a comment it is a deletion. If you restructure, keep the id on any element whose words survive, even where it has moved or been re-tagged. Committing names every id that is no longer present, so check each one and put back any that should have survived. Wrap each sentence in a <span>, so a comment has a natural unit to attach to. No browser storage — no localStorage, no cookies. The page has no internet. Everything it loads is a file inside the presentation, by relative path — Google Fonts is the only exception, and data: URIs are refused. fetch, XHR and WebSockets fail too. Links are the one way out: they may point anywhere, and Anchor handles the click, so target and window.open do nothing. Tags, attributes and ARIA roles are allowlisted, and narrower than you would guess: no form controls, iframes, canvas or on* handlers, and <label>, <progress> and interactive roles are out too — except the few below that a reader's state lives on, which are the only controls a presentation has, whether or not it keeps what the reader does. A rejection lists everything that would have been accepted. Ordinary CSS is fine, inline in a <style> block or in a .css file beside the page. Charts and diagrams are SVG, which stays readable to search engines, diffs and printers. config.json says what the slides are, in the order they appear: { "kind": "presentation", "canvas": { "width": 1280, "height": 720 }, "tabs": [ { "id": "k3f9x2", "title": "Introduction", "file": "slide-1.html" }, { "id": "m7p2qd", "title": "How it works", "file": "slide-2.html" } ] } Each entry: `id` is a stable handle, `title` is what Anchor's chrome shows, `file` is a .html file at the root of the folder. **An `id` is opaque and permanent.** Invent about six characters with no relation to the title — a letter, then letters or digits, like `k3f9x2` — and never change it again. It is what a link to this slide points at and what a reader's saved state is filed under, so changing one silently breaks both: a link that used to open here lands on the first slide instead. Retitling and reordering are free, and neither touches the `id`. Do not make it resemble the title, or it will look wrong the moment the title changes and invite exactly that edit. Pick a new `id` only for a slide that did not exist before. A presentation can show live rows from a table. Declare them in two more halves of config.json, beside `tabs` — the same `dependencies` and `functions` an app's functions.json uses, minus its `version` — and leave both out when there is no data to read: { "dependencies": [ { "id": "targets", "type": "TABLE", "file_id": "<the table's file id>", "access": "READ", "description": "Reads quarterly targets for the table on slide 1." } ], "functions": [ { "type": "SQL", "name": "quarterlyTargets", "description": "Every target, by quarter", "sql": "SELECT quarter, target, actual FROM \"<the table's file id>\" ORDER BY quarter", "inputSchema": [], "outputSchema": { "quarter": { "type": "string", "description": "Quarter, as 2026-Q1" }, "target": { "type": "number", "description": "Target, in USD" }, "actual": { "type": "number", "description": "Booked so far, in USD" } } } ] } A dependency's `id` is a short lowercase name of your choosing, unique in the file. Its `description` is what the table's owner reads when they decide whether to allow this, so write it for them. Every function is `"type": "SQL"`, and `inputSchema` must be empty: nobody is there to supply an argument, and a query with parameters would return nothing. Every query runs as written, for each reader, with only that reader's own access — a reader who cannot see the table sees no rows, and the presentation cannot lend them any. The rows are handed to the page. There is nothing to call and no endpoint to reach: each query's rows arrive as an `anchor:data` event, and the latest rows for each name stay readable at `window.anchorData(name)`, so a script that starts listening late has missed nothing. Do both, or the page is blank whenever delivery wins the race: <script id="targets-draw"> function draw(rows) { /* replace the contents of one container */ } window.addEventListener('anchor:data', function (event) { if (event.detail.name === 'quarterlyTargets') draw(event.detail.rows); }); var rows = window.anchorData && window.anchorData('quarterlyTargets'); if (rows) draw(rows); </script> The event fires again whenever the table changes, so draw by replacing what you drew, never by appending. Rows drawn this way are not in the markup, which means they are not in a print, a thumbnail or a search result — so the presentation's own words still belong in the DOM, and the container they land in needs markup that reads sensibly while empty. Ids are checked on what you commit, so rows built by script need none. A reader can interact with a presentation, and you decide whether what they do is kept. Both kinds are written the same way — there are no form controls, so anything interactive is an element with an ARIA role and a listener — which is exactly why the choice has to be made on purpose. **Kept, and shared with everyone.** `anchor.set(id, attribute, value)` sets the attribute and stores it as a new version of the presentation: every reader sees it, it survives a reload, and it can be undone. This is what a shared checklist is — somebody ticks `Tests written` and their colleagues open the presentation and find it ticked. <span id="tests-done" role="checkbox" aria-checked="false" tabindex="0">Tests written</span> <script id="tests-done-script"> var box = document.getElementById('tests-done'); box.addEventListener('click', function () { var next = box.getAttribute('aria-checked') === 'true' ? 'false' : 'true'; anchor.set('tests-done', 'aria-checked', next); // stored, and everyone sees it }); </script> **Not kept, and nobody else's business.** Change the page yourself and the change lives in that one reader's browser until they reload. That is the right answer for anything only the person reading it should see: a mock-up of a feature inside a spec, a worked example somebody steps through. Nobody wants a colleague's click on a mock saved into the document as a new version. <div id="demo-card" class="card">Your first project</div> <span id="demo-dark" role="switch" aria-checked="false" tabindex="0">Preview in dark mode</span> <script id="demo-dark-script"> var demo = document.getElementById('demo-dark'); demo.addEventListener('click', function () { var on = demo.getAttribute('aria-checked') !== 'true'; demo.setAttribute('aria-checked', String(on)); // this reader only, gone on reload document.getElementById('demo-card').classList.toggle('dark', on); }); </script> **They look the same until somebody reloads, so ask one question:** should a second reader see this change? If yes it goes through `anchor.set`; if no it does not. A shared checklist built without it works perfectly for the person who ticked it and is empty for everyone else, and neither of them will find out until one of them reopens the presentation. Stored state is read back out of the markup, where it is — not out of a variable you kept alongside it. What can be stored is a state, on an element whose role owns it: `aria-checked` on a checkbox, option, radio or switch; `aria-expanded` on a checkbox, columnheader, listbox, row or rowheader; `aria-selected` on a columnheader, option, row or rowheader; `aria-valuenow` on a slider, within the `aria-valuemin` and `aria-valuemax` you gave it. Anything else is refused and the reason is logged in the page's console. Write the starting value into the markup at commit. It is the state, so a presentation that has never been opened already reads correctly — and it is what the print, the thumbnail and a search engine see. Saving needs edit access. A reader with view access sees the control and the change is put back, which is a good reason not to build one that looks disabled when it is not. A presentation is fixed-layout. Lay every slide out against the pixel canvas in config.json (1280x720 unless it says otherwise) and position things absolutely against it — the viewer scales the whole slide to fit, so responsive CSS is not needed and will not help.
commit_presentation
Copy a file. Only files can be copied (not folders). The copy gets a new ID, the caller becomes the owner, and no existing access roles are carried over. For tables, the full PostgreSQL table (schema + data) is duplicated. For documents and presentations, only the latest version is copied and becomes version 1 of the copy, with the tables it reads declared again. For apps, the full version history (every version's source, dependencies, and built bundle) is duplicated and the copy gets a fresh slug; copying an app requires contributor access to the source, since the copy's owner can unpack its source code. Requires read access to the source file and write access to the destination folder. Only in-org copies are allowed. An OTHER file may be copied into, out of, or between temporary folders (the scratch folders `mkdir` creates with `temporary: true`); nothing else — a table, an app, a document or a presentation — can enter one.
cp
Create a new folder. Pass folder_id to create a subfolder inside an existing folder, or org_id to create a folder at the org root. Requires permission at the parent location. Set temporary: true to create a temporary folder instead — private scratch space, visible only to you, always at the org root (org_id required, folder_id forbidden). It is where files are staged before build_app, commit_document or commit_presentation. A temporary folder persists until one of those succeeds against it and deletes it; there is currently no other way to remove one, so an abandoned temporary folder sits there permanently — only create one when you are about to populate and commit it. If what you stage will call tools on an MCP server, call list_mcp_servers for the org first: a build or commit that declares a server the org has not approved is refused, and the folder stays.
mkdir
Create a new table in Anchor (File type: TABLE). Tables are structured data stores with typed columns. Under the hood, this creates a PostgreSQL table whose name is the returned UUID. The table is displayed to users without exposing database concepts, but all column configurations (types, constraints) map directly to PostgreSQL features — the Postgres type is noted next to each config option. Tables are designed to work together through normalization: - Master data entities (Customers, Products, Employees, Vendors, Locations, Categories, etc.) are the core reusable "nouns" of an organization. - Transactional tables (Orders, Invoices, Appointments, Transactions) point at master tables rather than embedding fields like "customer_name" or "product_name" directly. Store a relationship as an INTEGER column (e.g. "customer_id") holding the other table's record_id, and set linkedTableId to the other table's UUID so readers know which table to join. The database does not enforce the link. - Existing tables in the organization can be inspected with ls and describe_table to find the tables to point at and avoid duplicating master data. SCHEMA RULES: - Every table must include a "record_id" column (INTEGER, isUnique: true, isRequired: true). This column is auto-generated (GENERATED ALWAYS AS IDENTITY) — do NOT include record_id values in subsequent INSERT queries. - isRequired: true needs a defaultValue of the column's type (except record_id and CREATED_BY_EMAIL): "" for STRING, 0 for NUMBER/INTEGER, false for BOOLEAN, a date like "2024-01-01" for DATE. No sensible default? Use isRequired: false. - Columns with linkedTableId: use isRequired: false. 0 matches no record_id. - Each column has an identifier (snake_case, immutable, used in SQL) and a display_name (human-friendly, shown in UI). - Each column uses exactly one of two forms: standard, or ENUM (requires possibleValues). - columns is an array of column definitions (preferred). A legacy map keyed by identifier is also accepted. - The full column schema is set at creation time. After creating, use describe_table to inspect the schema. Example input: { "folder_id": "0195e7a1-b0a0-7f00-a1b2-c3d4e5f60001", "name": "Tasks", "description": "Tracks work items and their status", "columns": [ { "identifier": "record_id", "display_name": "Record ID", "type": "INTEGER", "isUnique": true, "isRequired": true }, { "identifier": "name", "display_name": "Name", "type": "STRING", "isUnique": false, "isRequired": true, "defaultValue": "" }, { "identifier": "due_date", "display_name": "Due Date", "type": "DATE", "isUnique": false, "isRequired": false }, { "identifier": "priority", "display_name": "Priority", "type": "ENUM", "isUnique": false, "isRequired": true, "possibleValues": ["P0", "P1", "P2"], "defaultValue": "P2" } ] }
create_table
Permanently delete any Anchor file — a table, an app, a document, a presentation, or an other-file (HTML, Markdown, CSV, etc.). The deletion is immediate and irreversible — there is no trash or undo. For a TABLE, the underlying data is dropped along with the file; for everything else, the stored content goes with it, including every version of a document, presentation or app. Only an owner of the file (or of a parent folder) can delete a file. This is a destructive action — confirm with the user before deleting files you did not create.
rm_file
Permanently delete a column from an existing Anchor table. The deletion is immediate and irreversible — all data in the column across every row is destroyed and cannot be recovered. This is a destructive action — confirm with the user before deleting a column. Under the hood this runs an ALTER TABLE … DROP COLUMN on the underlying PostgreSQL table: ALTER TABLE anchor_tables."<table_uuid>" DROP COLUMN <column_identifier>; The "record_id" column cannot be deleted — it is the primary key.
delete_table_column
Get a table's full schema, metadata, and row count. Use this to understand a table's structure before querying or modifying it with query_table, add_table_column, or delete_table_column. Does not return row data — use query_table for that. For tables with many rows, use query_table to peek at a sample or write aggregated SQL queries rather than fetching all rows. Example input: { "table_id": "a195e7a1-b0a0-7f00-a1b2-c3d4e5f60001" } Returns: - Row count: total number of rows in the table. For large tables, use query_table with LIMIT to peek at sample data, or write aggregated SQL queries (COUNT, SUM, AVG, GROUP BY, etc.) instead of selecting all rows. - Column definitions: each column has an identifier (snake_case, immutable — use this in SQL queries) and a display_name (human-friendly — use this when communicating with the user). Also includes types, constraints, allowed values, and the table each column links to (linked_table_id, not enforced). - Timestamps: creation and last update times.
describe_table
Map of your orgs, folders, and files. A good starting point when the layout isn't already known. If you're not sure where to begin, omit `target` (or pass {by:"overview"}) — it lists every org you belong to plus "Shared with Me", each expanded breadth-first under number_of_items (default 20). To scope in, set `target` to exactly one tagged variant: - {by:"overview"} → every org + "Shared with Me" (the default). - {by:"org", org_id} → that org's root folders, expanded breadth-first. - {by:"folder", folder_id} → that folder, then its children, expanded breadth-first. - {by:"file", file_id} → one file's details (leaf — no recursion). For an APP file, also lists its source file names — read-only here; call unpack_file to get an editable copy in a temporary folder, then read_file/write_file on the copies there. - {by:"shared_with_me"} → folders and files shared with you from outside your orgs. - {by:"slug", slug} → resolve an app from its serving URL (e.g. "a1b2" or "a1b2-v3"); behaves like {by:"file"} on the resolved app. A bare slug needs consumer access; a -vN suffix needs contributor access (it previews an unpublished version). A listing that shows an app as a row does not repeat its source-file list — call ls again with {by:"file", file_id} (or {by:"slug"}) on that app to see it. Reading the output (indented text, not JSON): - Lines starting with "- " are items. Lines without "- " are properties of the nearest item above them. - Indentation is 2 spaces per level; a child sits one level deeper than its parent. - Folders end with "/". Files do not. Orgs are prefixed with "[Org] ". "Shared with Me" has no id. - Every item shows its uuid after " -- " (except "Shared with Me"). - "# items: N" under a folder/org is its total visible child count. If fewer rows appear beneath it, the rest were trimmed by number_of_items — call ls with target {by:"folder",folder_id} to drill in. - A trailing "... N more orgs not shown" line means the overview hit number_of_items before listing every org. "Shared with Me" is always shown; raise number_of_items or scope in with {by:"org",org_id}. Example: - [Org] Neural Bridge -- 019385a0-0000-0000-0000-000000000001 # items: 5 - Home/ -- 019385a0-0000-0000-0000-000000000002 # items: 2 - file1 -- 019385a0-0000-0000-0000-000000000010 - file2 -- 019385a0-0000-0000-0000-000000000011 Tunables (all optional): - number_of_items (default 20): global budget across orgs + folders + files. - show_org_details / show_folder_details / show_file_details / show_shared_with_me_details: add property blocks (timestamps, type, sizes, versions, …) per item type. - hide_orgs / hide_folders / hide_files / hide_shared_with_me: drop a whole category from the output and from the budget. - show_temporary (default false): include temporary folders when browsing an org/overview. A known temporary folder id always resolves via {by:"folder"} regardless of this flag — it only gates unprompted discovery. IDs appear in Anchor URLs: - https://anchor.cc/org/$org_id - https://anchor.cc/folder/$folder_id - https://anchor.cc/file/$file_id — also /table/, /app/, /document/ and /presentation/$file_id - https://anchor.cc/shared-with-me — target {by:"shared_with_me"}
ls
Move or rename a file or folder. Source can be a file or folder. Destination can be an org (root level) or a folder — but files cannot be moved directly under an org, only into folders. Requires editor (or owner) access on the source and on a destination folder; moving a folder to an org root only requires membership in that org. An OTHER file may be moved into, out of, or between temporary folders (the scratch folders `mkdir` creates with `temporary: true`); apps and tables can never enter a temporary folder, and a temporary folder itself can never be a move source or destination as a folder. Valid argument combinations (set exactly one source; unused fields must be omitted or null): • Move a file into a folder: { file_id, destination_folder_id, new_name? } • Rename a file in place: { file_id, new_name } • Move a folder into a folder: { folder_id, destination_folder_id, new_name? } • Move a folder to an org root: { folder_id, destination_org_id, new_name? } • Rename a folder in place: { folder_id, new_name } Constraints: set exactly one of file_id / folder_id. Provide at least one of destination_folder_id, destination_org_id, or new_name. destination_org_id is only valid for folder sources (files cannot live at the org root). destination_folder_id takes precedence over destination_org_id when both are set.
mv
Returns a presigned URL to download a file's raw bytes. GET it yourself (e.g. curl). Preferred over read_file for any file when you can reach the URL. If the GET fails, read_file is the fallback: it only works for textable files, and returns extracted text for PDF, DOCX, PPT, and XLS. Files that are not textable need to be downloaded from the Anchor UI. The URL expires in 15 minutes and allows GET only.
download_file_presigned_url
Returns a presigned URL to upload file bytes to a staging key in one folder. This is the preferred way to move any file content into Anchor (html, md, json, code, binaries), but it works only if you can make HTTP requests to the URL. Confirm that with step 2 before generating anything large. Flow: 1. Call this tool with the folder the file will live in. For a replacement that is the existing file's parent folder — ls the file to read its parent_folder_id. You get a URL valid for 2 hours, bound to that folder. 2. PUT a tiny test body to it (e.g. `curl -X PUT --data-binary 'hello' "<url>"`). If that fails, use write_file with text_content instead, typed straight into the tool call argument, not into a local file first (text formats only; binary content has no fallback). 3. PUT the real bytes to the same URL. 4. Call write_file with presigned_url to turn the bytes into a file: { folder_id, name, presigned_url } for a new file, { file_id, presigned_url } to replace an existing file's content. The folder_id must be the folder this URL was minted for, and a file_id's parent folder must be that same folder — otherwise the write is rejected. One URL per file: write_file consumes the URL's upload, so call this tool again for the next file.
upload_file_presigned_url
Run a SQL query against Anchor tables. This executes real SQL against the underlying PostgreSQL database — every Anchor table is a Postgres table whose name is its UUID. ALLOWED STATEMENTS: SELECT, INSERT, UPDATE, DELETE (data operations only). BLOCKED STATEMENTS: CREATE, ALTER, DROP, TRUNCATE, triggers, indexes, and all other schema/DDL operations — these are rejected. Use create_table, add_table_column, or delete_table_column for schema changes. ONE STATEMENT PER CALL: - Exactly one SQL statement per call: no ";"-stacked statements, no BEGIN/COMMIT wrappers (each call already runs in its own transaction). - Bulk-insert with a single multi-row VALUES list. - For upserts use INSERT ... ON CONFLICT (MERGE is not allowed). - Inline literal values; bind parameters ($1) are not supported. ACCESS CONTROL: - Viewer access (read-only): SELECT queries only. - Editor access (read + write): SELECT, INSERT, UPDATE, and DELETE. If a user only has viewer access, write queries will be rejected with an access error. REFERENCING TABLES AND COLUMNS: - Tables are referenced by UUID as a double-quoted identifier: SELECT * FROM "a195e7a1-b0a0-7f00-a1b2-c3d4e5f60001" - Columns are referenced by their snake_case identifier (no quoting needed). - Always call describe_table first to discover column identifiers, types, and constraints before writing queries. - Every query must reference at least one Anchor table (a bare SELECT 1 is rejected). - System catalogs (pg_catalog, information_schema) are not queryable; use describe_table for schema discovery. CROSS-TABLE JOINS: You can JOIN multiple tables in a single query as long as all tables belong to the same organization. Cross-org queries are not allowed. RECORD_ID: The record_id column is auto-generated (GENERATED ALWAYS AS IDENTITY). Never include record_id in INSERT statements — it is assigned automatically. You can use record_id in WHERE clauses, JOINs, and SELECT lists. Add RETURNING record_id to an INSERT to get the generated ids back. CREATED_BY_EMAIL COLUMNS: Columns of type CREATED_BY_EMAIL are enforced server-side by a trigger that overrides the column with the calling user's email on every INSERT and UPDATE. You usually don't need to include these columns in writes — INSERT without them and the trigger will populate them automatically. If you do include a CREATED_BY_EMAIL column in an INSERT or UPDATE, you MUST set it to your own email (the email of the user making the request); any other value will be silently overwritten with your email by the trigger, so inserting or updating a peer's email will not work. DESTRUCTIVE OPERATIONS (UPDATE, DELETE): Before running UPDATE or DELETE queries, ALWAYS confirm with the user first. Describe what rows will be affected (e.g. "This will delete 3 rows where status = 'archived'") and wait for explicit approval. Data modifications cannot be undone. EXAMPLES: Select all rows: SELECT * FROM "a195e7a1-b0a0-7f00-a1b2-c3d4e5f60001" Select with filter: SELECT name, email FROM "a195e7a1-b0a0-7f00-a1b2-c3d4e5f60001" WHERE status = 'active' ORDER BY name Insert a single row (omit record_id): INSERT INTO "a195e7a1-b0a0-7f00-a1b2-c3d4e5f60001" (name, email, status) VALUES ('Alice', 'alice@example.com', 'active') RETURNING record_id Insert multiple rows: INSERT INTO "a195e7a1-b0a0-7f00-a1b2-c3d4e5f60001" (name, email, status) VALUES ('Alice', 'alice@example.com', 'active'), ('Bob', 'bob@example.com', 'pending') Join two tables (same org): SELECT o.order_date, c.name AS customer_name, o.total FROM "a195e7a1-b0a0-7f00-a1b2-c3d4e5f60001" o JOIN "a195e7a1-b0a0-7f00-a1b2-c3d4e5f60002" c ON o.customer_id = c.record_id WHERE o.total > 100 Aggregate query: SELECT status, COUNT(*) AS count FROM "a195e7a1-b0a0-7f00-a1b2-c3d4e5f60001" GROUP BY status Update rows (confirm with user first): UPDATE "a195e7a1-b0a0-7f00-a1b2-c3d4e5f60001" SET status = 'archived' WHERE last_login < '2024-01-01' Delete rows (confirm with user first): DELETE FROM "a195e7a1-b0a0-7f00-a1b2-c3d4e5f60001" WHERE status = 'archived'
query_table
Reads the text content of an OTHER file where textable is true. Text formats (Markdown, HTML, JSON, CSV/TSV, XML/YAML, plain text, source code) come back verbatim, so oldString edits via write_file match exactly. PDF, DOC/DOCX, PPT/PPTX, XLS/XLSX, RTF come back as extracted plain text. Prefer download_file_presigned_url when you can reach its URL; use read_file when you want that extracted text rather than raw bytes, or need a file's current content for a surgical edit. TABLE files: describe_table or query_table. Apps, documents and presentations: unpack_file, then read_file the sources.
read_file
Writes an Anchor app, document or presentation back out as editable files in a new temporary folder, so it can be read, changed and saved as a new version. Returns the folder id, every file written with the id read_file and write_file need, and which tool saves it: build_app for an app, commit_document or commit_presentation otherwise. View access is enough, which makes this the way to read how an app works or what a document says; a viewer of an app gets its published version only. Saving the folder as a new version still needs edit access, and can_save_new_version says whether you have it. For tables use query_table; for anything else use read_file. Version: null unpacks an app's published version — what its users see — and a document's or presentation's current one. Pass a number for any other version; ls on the file shows the latest. To restore an older version of a document or presentation, unpack it by number and commit the folder unchanged with the base_version this returns. An app's version is unpacked as its own source files only: build artifacts (bundle.html, fetch.ts) are regenerated by build_app, and OTHER files the app declares in functions.json stay where they are, referenced by file_id. build_app can save the folder as a new version of the same app, or fork it into a new one. Edit the folder with write_file, cp, mv and rm_file. The folder is deleted when the save succeeds; an abandoned one stays, invisible to the user and with no anchor link, so do not direct them to it.
unpack_file
Each file becomes a live, shareable page (its `view_link`), so prefer writing to Anchor whenever the user will want to view, keep, or share what you produce. Create or edit one or more OTHER files in a single call. Pass `writes`: an array where each entry independently creates a new file (folder_id + name) or edits an existing one (file_id), with content sourced either inline (text_content) or from a prior presigned upload (presigned_url) — the two are mutually exclusive per entry. Malformed entries are rejected by schema validation before anything runs (the whole call fails — fix the entry and retry). Once the batch starts, entries are applied sequentially in array order; each gets its own per-item result so a single runtime failure (e.g. NO_ACCESS, EDIT_CONFLICT) does not halt the rest of the batch — the response array preserves the same length and order as the input. Two content sources, presigned_url is the default: • presigned_url: the preferred source for any file type, including text formats like html and md, whenever you can reach the URL it returns. Mime is detected from the uploaded bytes, extension as fallback. • text_content: the fallback when the network probe fails, or the source for a surgical oldString/newString edit, since presigned_url only ever replaces a file's entire content, never a diff. Type the content straight into the tool call argument, not into a local file first; the only reason to write a local file is to PUT it to a presigned URL. Text formats only: Markdown, plain text, JSON, HTML, CSV/TSV, XML, YAML, TOML, SQL, and source code (.tsx, .ts, .js, .css, .py, .go, .rs, .java, .sh, etc.). Mime is detected from content + extension on every create and full rewrite; anything else is rejected with NOT_TEXTABLE. Per-entry shape — exactly one of the following five forms (enforced by the input schema; each form accepts only its listed fields): • Create new, inline: { folder_id, name, text_content: { newString } }. • Edit existing — surgical: { file_id, text_content: { newString, oldString, replaceAll? } }. oldString must match exactly once unless replaceAll is true; include surrounding context to keep oldString unambiguous. • Edit existing — full rewrite: { file_id, text_content: { newString, rewrite: true } }. Replaces the entire file with newString. • Create new, from a presigned upload: { folder_id, name, presigned_url }. Uses the bytes you PUT to a URL from upload_file_presigned_url, minted for this same folder_id. • Replace existing, from a presigned upload: { file_id, presigned_url }. Always replaces the whole file, like rewrite: true. Mint the URL with the folder the file lives in. Presigned writes. Get a URL from upload_file_presigned_url, passing the folder the file lives in (for a replacement, the folder the existing file is already in). PUT a tiny test body to it first, then PUT the real bytes, then call write_file with that URL. If the test PUT fails you cannot reach the URL: use text_content instead. The URL is bound to the folder it was minted for: a folder_id that differs, or a file_id whose parent folder differs, is rejected with UPLOAD_FOLDER_MISMATCH. One URL per file — write_file consumes the upload, so mint a new URL for the next file. Temporary folders as a write target. A temporary folder (created by mkdir with `temporary: true`, or by unpack_file) is a normal write target — create and edit files inside it exactly as in any other folder, same create / surgical-edit / full-rewrite mechanics (text_content or presigned_url). Two rules apply only inside a temporary folder: sibling names are compared case-insensitively (`Index.tsx` collides with `index.tsx`, rejected with TEMPORARY_FOLDER_NAME_CONFLICT), and a temporary folder's total content is capped at 100 MB (rejected with TEMPORARY_FOLDER_FULL). This is where an app's source is authored before calling build_app: `index.tsx`, `favicon.json`, `functions.json`, optionally `index.css`, plus any helper modules. Authoring `functions.json` and `favicon.json`. Both are plain JSON text written the same way as any other file. Inside a temporary folder each write is checked against the file's schema the moment it lands (see "Write-time checks" below), so a slip shows up in the same response instead of at build_app. The SQL inside functions.json is a string nested inside a JSON string, so get these two escaping rules right: • Table UUID → double-quoted in SQL → escaped in JSON. A table is referenced by its file UUID as the literal Postgres table name, and hyphens make it an invalid unquoted identifier, so it must be double-quoted in the SQL: FROM "a198a3ac-...". Since that whole SQL string is itself a JSON string value, those double quotes need JSON escaping: \"a198a3ac-...\". • Nothing else needs extra escaping. Column names (product_id, record_id) are plain lowercase identifiers — no quoting in SQL, so nothing to escape in JSON. SQL string literals use single quotes ('like this') — JSON escaping only applies to the surrounding double quotes of the JSON string itself, never to the single quotes inside it. functions.json has three top-level fields: • `"version": 2` — always exactly 2. • `dependencies` — every table and OTHER file the app touches. Each entry has an `id` (a lowercase identifier such as "products", unique within the file), a `type` ("TABLE" or "OTHER"), the file's `file_id`, and a `description` written for that file's owner. TABLE entries also carry `access` ("READ" or "WRITE"); OTHER entries (images and other non-table files) have no `access` field. There is no separate file for OTHER dependencies. • `functions` — the functions the app calls. Each has `"type": "SQL"`, a camelCase `name` unique within the file, a `description`, the `sql`, an `inputSchema`, and an `outputSchema`. Worked example — functions.json: { "version": 2, "dependencies": [ { "id": "products", "type": "TABLE", "file_id": "a198a3ac-1111-7abc-9def-000000000001", "access": "READ", "description": "Reads the product catalog to render the product picker." }, { "id": "sales", "type": "TABLE", "file_id": "a198a3ac-2222-7abc-9def-000000000002", "access": "WRITE", "description": "Inserts one sale row per checkout; never edits existing sales." } ], "functions": [ { "type": "SQL", "name": "getProducts", "description": "Retrieves all products", "sql": "SELECT * FROM \"a198a3ac-1111-7abc-9def-000000000001\" ORDER BY record_id", "inputSchema": [], "outputSchema": { "record_id": { "type": "integer", "description": "Auto-generated row ID" }, "name": { "type": "string", "description": "Product name" } } }, { "type": "SQL", "name": "insertSale", "description": "Insert a sale for the signed-in cashier", "sql": "INSERT INTO \"a198a3ac-2222-7abc-9def-000000000002\" (product_id, cashier_email) VALUES ($1, $2) RETURNING record_id", "inputSchema": [ { "name": "product_id", "type": "INTEGER", "description": "Product record id", "isNullable": false }, { "name": "cashier_email", "type": "EMAIL", "description": "Email of the signed-in cashier", "isNullable": false, "requireAuth": true } ], "outputSchema": { "record_id": { "type": "integer", "description": "New sale row id" } } } ] } Worked example — favicon.json (simpler, no SQL to escape): { "lucide_icon_name": "rocket", "foreground_color": "#FFFFFF", "background_color": "#1a1a1a" } Mounting `index.tsx`. index.tsx is the bundle's entry module and is responsible for mounting. The served page contains an empty <div id="root"></div> and runs your compiled bundle as a module script; the platform adds no bootstrapping code. End index.tsx with createRoot(document.getElementById('root')!).render(<App />), importing createRoot from 'react-dom/client'. A file that only exports a component produces a blank page, and the build rejects it with NO_ROOT_MOUNT. import { useEffect, useState } from 'react'; import { createRoot } from 'react-dom/client'; import './index.css'; import { listTasks } from './fetch'; function App() { const [rows, setRows] = useState<Awaited<ReturnType<typeof listTasks>>>([]); useEffect(() => { listTasks().then(setRows).catch(console.error); }, []); return <ul>{rows.map((r) => <li key={r.record_id}>{r.title}</li>)}</ul>; } createRoot(document.getElementById('root')!).render(<App />); // mounts the app export default App; // ❌ exports a component; #root stays empty and the page is blank The mount can live in a helper module instead, reached from index.tsx by a bare `import './mount'` or through an import whose names index.tsx actually uses. The build checks the compiled bundle, and the bundler drops a module when nothing imported from it is used, together with everything that module imports, so a mount behind an unused import is dropped too and the build fails with NO_ROOT_MOUNT. A helper's relative imports are limited to `./fetch`, `./index.css` and the other helper files, so it cannot reach the component through `./index`; put the component in its own file that both can import. Calling backend functions from `index.tsx`. build_app turns each `functions` entry into one exported helper in `./fetch` — same name, POSITIONAL parameters in `inputSchema` order, always resolving to an array of rows. You never write fetch.ts yourself. import { getProducts, insertSale } from './fetch'; await getProducts(); // inputSchema [] → zero arguments await insertSale(42); // one argument per inputSchema entry, in order // cashier_email is requireAuth → optional, sorts last, omit it and // the server injects the caller's email await insertSale({ product_id: 42 }); // ❌ NEVER an object. Builds fine (build_app does not typecheck), // then every call fails with INVALID_REQUEST. Rows come back keyed by the outputSchema column names ([] for a mutation with no RETURNING). A failed call throws `Error` whose message is the server code — CONSENT_REQUIRED (normal until each user consents), NOT_AUTHORIZED, INVALID_REQUEST, BUDGET_EXCEEDED — so catch it and show the user. Also exported: `anchorOtherFileFetch(fileId)` → a URL for a declared OTHER dependency, used directly as an src/href, and `listenToDataUpdates(onChange)` → live change notifications for the app's tables (the callback receives the changed table's file id whenever any user edits it; refetch through the `./fetch` functions in response — the event carries no data). It returns an unsubscribe function, so in React: `useEffect(() => listenToDataUpdates(() => { loadTodos(); }), [])`. Use it whenever the app displays table data that other users can change. Images and other assets in index.tsx. Never base64-encode an image into a source file. Instead: (1) the image must already be an OTHER file in a regular folder — text_content cannot carry an image (NOT_TEXTABLE) and a file inside a temporary folder fails the build with OTHER_DEPENDENCY_IN_TEMPORARY_FOLDER; find it with ls, or have it uploaded into a regular folder first. (2) Declare it in functions.json's dependencies as { "id": "logo", "type": "OTHER", "file_id": "<uuid>", "description": "..." } — no access field, and no functions entry is needed (the unused-dependency check is TABLE-only). (3) Render it with `<img src={anchorOtherFileFetch(fileId)} alt="..." />`, importing anchorOtherFileFetch from ./fetch; the URL only serves ids declared by the served version. Each user needs their own direct CONSUMER access to the image file (no via-app grant for OTHER files), so keep it in a folder the app's users can read. Editing `functions.json` or `favicon.json`: prefer `rewrite: true` over a surgical edit. A surgical `oldString`/`newString` edit that leaves the file invalid still lands (with a warning, below), and adding a `functions` entry via a surgical edit means getting comma placement and bracket nesting right by hand across a diff; a full `rewrite: true` of the whole file is much harder to corrupt, since you are looking at the complete, valid JSON about to be submitted rather than a fragment. Use a surgical edit here for a single, unambiguous one-line value swap — including the exact field a warning names. Write-time checks in temporary folders. Every file written into a temporary folder is checked the moment it lands, with the same checks build_app runs: `functions.json` and `favicon.json` are parsed against their schemas, and every `.ts`/`.tsx`/`.js`/`.jsx` file must parse and may import only allowlisted packages, `./fetch`, `./index.css`, and helper modules present in the folder. Checks run after the whole batch, so writing index.tsx before its helper in one call is fine. The write always lands. A failed check adds `warnings: [{ code, detail }]` to that entry's success result — INVALID_FUNCTIONS_JSON, INVALID_FAVICON_JSON, or SOURCE_VALIDATION_FAILED — and `summary.warned` counts such entries; the batch `message` says so too. `detail` names the failing field or import, so fix exactly that and write again — build_app fails on the same file with the same detail. For the two JSON files a schema rejection names the exact field, while a file that never parsed names none, so rewrite that one whole rather than patching it. Each file is checked once in its final state, so two entries writing the same file in one call produce at most one warning, on the later entry. The checks apply inside temporary folders only; the same names in a regular folder are ordinary text files. Inside a temporary folder the names bundle.html and fetch.ts are refused with RESERVED_FILE_NAME — build_app emits them. A name build_app would reject when it hydrates the folder is refused the same way, with INVALID_APP_FILE_NAME: app source files are all root-level, so components/Card.tsx is written as Card.tsx and imported as './Card'. Names are refused rather than warned, since a warning would leave the unusable file in the folder. Picking a format for documents and presentations. Default to HTML (.html) for documents, write-ups, and slides — full layout control that Markdown's constrained renderer can't match. Use .md only when explicitly requested or for README/changelog-style source. Markdown contract (`.md` files). Markdown files are rendered with GitHub-Flavored Markdown (GFM): headings (`#`–`######`), lists, task lists (`- [ ]`), tables, fenced code blocks, blockquotes (`>`), autolinks, strikethrough (`~~`). A limited HTML subset is supported (the same allowlist GitHub uses: `<h1>`–`<h6>`, `<p>`, `<blockquote>`, `<details>`/`<summary>`, `<pre>`, `<code>`, `<kbd>`, `<sub>`/`<sup>`, `<table>` family, `<a>`, `<img>`, `<hr>`, `<br>`, `<em>`, `<strong>`, `<del>`, `<ins>`). The `style`, `class`, and `id` attributes are stripped, as are `<script>`, `<iframe>`, `<object>`, `<embed>`, and bare `<div>`/`<span>` wrappers — do NOT rely on inline CSS or layout HTML; it will be silently removed and the document will collapse to plain text. Write semantic Markdown (`## Heading`, `> blockquote`, `**bold**`, lists, tables) — the renderer applies consistent typography, spacing, and color tokens automatically. Designing a custom font/color/border per document is an anti-pattern: the doc will look broken in the Anchor preview and unportable everywhere else. HTML contract (`.html` files). HTML files are rendered inside a full-screen iframe with no surrounding chrome and no parent-supplied padding — the document IS the viewport. Author every .html as a complete, self-contained page: • Include `<!doctype html>`, `<html>`, `<head>`, and `<body>`, and a viewport meta tag: `<meta name="viewport" content="width=device-width, initial-scale=1">`. • Put padding/margin on `body` (or a single content wrapper) so text does not touch the iframe edges. Constrain a readable measure (e.g. `max-width: 72ch; margin: 0 auto;` for prose; full-bleed sections for slides) so long lines don't sprawl on wide screens. • Be mobile-first and responsive. Mobile viewports (≤ 480px wide) are common — use fluid units (`rem`, `%`, `vw`/`vh`, `clamp()`), media queries, and flex/grid that wraps. Never assume a desktop width; never produce horizontal scroll on a phone. • For slide-like presentations, build each slide as a full-viewport section (`min-height: 100vh`) and let the user scroll between them; keep titles, bullets, and imagery readable on a narrow viewport. • Inline `<style>` and `<script>` are allowed and encouraged — the iframe sandboxes the document, so there is no parent CSS to inherit or conflict with. Choose colors, fonts, and spacing deliberately; nothing is applied for you. • Always use relative hyperlinks for inter-document links — never hardcode absolute URLs (e.g. `https://anchor.cc/...`) for content that lives in the same folder. Use relative paths (`./other-file.html`) for sibling files. Absolute links break when the document is moved, copied, or shared, while relative paths stay portable. In-page navigation is not supported (the document is rendered inside an iframe), so do not rely on `href="#..."` fragment anchors for tables of contents or cross-references. • Links to a different page or external site MUST add `target="_blank" rel="noopener"` — the document lives in an iframe, so a plain link loads the destination inside that frame instead of as a full page. Constraints: ≤ 100 writes per call. Inline rewrites that produce non-textable content are rejected with NOT_TEXTABLE (this restriction does not apply to presigned_url writes). Each resulting file must stay under the platform file-size ceiling, else the entry is rejected with FILE_TOO_LARGE. A presigned_url not from upload_file_presigned_url is rejected with INVALID_PRESIGNED_URL; one with no bytes at it is UPLOAD_NOT_READY, whether it was never PUT to or already consumed by a previous write_file (mint a new URL); one PUT to again after being consumed is UPLOAD_NOT_FOUND (mint a new URL); one minted for a different folder than this write targets is UPLOAD_FOLDER_MISMATCH. Inspect the returned `results` array: each entry is either a success object (file_id, name, mime_type, size_bytes, line_count, created_or_updated, plus `warnings` when a write-time check failed) or an error object ({ error, detail? }). The `summary` field reports per-status counts (total, created, updated, failed, warned) so the agent can see at a glance whether the batch fully succeeded and is ready to build.
write_file
Anchor ChatGPT Plugin FAQ
How the directory, categories and Discoverability Score work.
Read the methodologyHow do I improve Anchor's ChatGPT Plugin discoverability?
The levers are the listing surface agents actually read: names, descriptions, keywords, tool metadata, and registry health. Which lever matters depends on where discovery breaks, which is what continuous measurement shows.
What are Anchor alternatives on ChatGPT?
As of 2026-11-02, Anchor competes with Dropbox, Box, Context — ctxt.io, DocSend, Fastio, File Requests, FileAssist, FileFast and 8 more in ChatGPT Cloud File Storage, ranked by public Discoverability Score.
Where does Anchor rank in Cloud File Storage on ChatGPT?
As of 2026-11-02, Anchor ranks #3 of 17 in ChatGPT Cloud File Storage with a Discoverability Score of 0/100 (Invisible).
Where is this profile measured?
This profile uses the geography attached to the latest public registry snapshot: US. Locale tags are intentionally omitted.