- 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
- 16
- Geography
- US
The Primary Subcategory used for this profile’s headline score.
Other Subcategories where the Integration is listed.
Your score is coming
ChatGPT now suggests Plugins on its own when they match a user's request.Your Plugin Discovery Score measures how often yours appears, and it will show here as soon as it’s ready.
What discovery looks like

Get alerts for Anchor
Get updates when Anchor’s Discoverability Score or category rank changes.
Competing in ChatGPT Cloud File Storage
View Category16 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: { "table_id": "a195e7a1-b0a0-7f00-a1b2-c3d4e5f60001", "column": { "identifier": "project_id", "display_name": "Project", "type": "INTEGER", "isUnique": false, "isRequired": false, "foreignKeyTableIdentifier": "a195e7a1-b0a0-7f00-a1b2-c3d4e5f60002", "foreignKeyOnDelete": "CASCADE" } } 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 (...)] [REFERENCES <fk_table>(record_id) ON DELETE CASCADE|SET NULL]; 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 three forms: standard, ENUM (requires possibleValues), or foreign key (requires foreignKeyTableIdentifier + foreignKeyOnDelete). - 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 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_app 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
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 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`); apps and tables can never enter a temporary folder.
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 for app source code, visible only to you, always at the org root (org_id required, folder_id forbidden). Used to stage files before build_app. A temporary folder persists until a successful build_app run against it 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 build it.
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, foreign keys) 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) reference master tables via foreign keys (foreignKeyTableIdentifier) rather than embedding fields like "customer_name" or "product_name" directly. - Existing tables in the organization can be inspected with ls and describe_table to identify foreign key targets 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. - 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 three forms: standard, ENUM (requires possibleValues), or foreign key (requires foreignKeyTableIdentifier + foreignKeyOnDelete). - 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 a table, app, or 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 an OTHER file, the stored content is removed; for an APP, every version of its source and build output is removed. 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 foreign key references. - Timestamps: creation and last update times.
describe_table
Get links to view or download a file. The view link opens the file as a live, rendered page in the browser that a human can read and share. Files of type OTHER return both a view link and a download link; TABLE files return a view link only (they have no downloadable file); APP files return a view link to the app's management page only (an app has no single downloadable file).
file_link
Get a deep link where the user can manually upload files to a folder in the Anchor UI. Supports only files of type OTHER; tables are created by their dedicated tool.
file_upload_link
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_app 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/$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
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', '[email protected]', 'active') RETURNING record_id Insert multiple rows: INSERT INTO "a195e7a1-b0a0-7f00-a1b2-c3d4e5f60001" (name, email, status) VALUES ('Alice', '[email protected]', 'active'), ('Bob', '[email protected]', '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 parsed text content of an OTHER file (e.g. extracted text from PDFs, spreadsheets, documents, or text files written via write_file). Applies only to OTHER files where textable is true. Text-like formats (Markdown, HTML, JSON, CSV/TSV, XML/YAML, plain text, and source code) are returned as raw bytes verbatim — byte-for-byte identical to what write_file would round-trip, so oldString-based edits will match exactly. Binary document formats (PDF, DOC/DOCX, PPT/PPTX, XLS/XLSX, application/rtf) are returned as extracted plain text only — the original binary structure is lost and write_file cannot edit these formats. For TABLE files use describe_table or query_table. For APP files, use unpack_app to get its source as files, then read_file those. Works on files inside a temporary folder (e.g. to read back a file just written via write_file, before calling build_app).
read_file
Create a temporary folder and copy every source file from a chosen app version into it. Build artifacts (bundle.html, fetch.ts) are skipped — those are regenerated by build_app from source. The new folder's UUID is returned as temporary_folder_id, which you pass back to build_app. Only that version's own source files are copied. OTHER files the app declares in functions.json (images and the like) stay where they are: functions.json keeps referencing them by file_id, and nothing about them needs to be in the temporary folder. Requires contributor/owner access to the app — the source code is not exposed to viewers. Version selection: omit `version` to unpack the published version (the safer default — that is what end users see). Pass `version` explicitly to unpack a specific version. Recommended workflow: call ls on the app first to inspect published_version and latest_version, then pass version explicitly when iterating on unpublished work. After unpack, edit the temporary folder via write_file (surgical or rewrite), cp/mv to bring in or take out OTHER files, and rm_file to prune. Then call build_app(temporary_folder_id, app_id) to publish a new version of the same app, or build_app(temporary_folder_id, name, destination_folder_id) to fork a new app from the unpacked source. The temporary folder persists until a successful build_app run against it deletes it — there is currently no other way to remove one, so an abandoned temporary folder sits there permanently. The folder never renders in the web UI — it has NO anchor link and is invisible to the user; do not direct the user to it.
unpack_app
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 textable 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). 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. Writable mime types — text only. write_file accepts plain-text formats whose mime type is in the platform textable allowlist: Markdown (.md), plain text (.txt, .log, .ndjson), JSON (.json), HTML (.html), CSV/TSV (.csv, .tsv), XML (.xml), YAML (.yaml, .yml), TOML (.toml), SQL (.sql), and source code (.tsx, .ts, .js, .jsx, .css, .py, .go, .rs, .rb, .java, .c, .cpp, .cs, .php, .swift, .kt, .scala, .sh, .lua, .dockerfile, etc.). Binary document formats — PDF, DOC/DOCX, PPT/PPTX, XLS/XLSX, application/rtf — CANNOT be created or rewritten via write_file (they are read-only through read_file). Images, archives, and other binary content are likewise unsupported. The mime type is auto-detected from content + filename extension on every create and every full rewrite; if the detected type is not on the allowlist the entry is rejected with NOT_TEXTABLE. Per-entry shape — exactly one of the following three forms (enforced by the input schema; each form accepts only its listed fields): • Create new: { 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. Temporary folders as a write target. A temporary folder (created by mkdir with `temporary: true`, or by unpack_app) is a normal write target — create and edit files inside it exactly as in any other folder, same create / surgical-edit / full-rewrite mechanics. 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: `getUserInfo(email?)` → the signed-in user or null, `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. Rewrites that produce non-textable content are rejected. Each resulting file must stay under the platform file-size ceiling, else the entry is rejected with FILE_TOO_LARGE. 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-10-09, Anchor competes with Box, Dropbox, Fastio, FileAssist, FilesAnywhere, firestorage.ai, IDrive e2, ShareWatch and 1 more in ChatGPT Cloud File Storage, ranked by public Discoverability Score.
Where is this profile measured?
This profile uses the geography attached to the latest public registry snapshot: US. Locale tags are intentionally omitted.