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

# AGO Apps

> Build small web pages and APIs with Claude or Codex, hosted by AGO, opened with your AGO account and callable by your agents.

AGO Apps hosts small apps for your workspace: an internal web page for your team, or an API that your agents call. You write them with Claude, Codex or any coding agent; AGO builds and deploys every change, signs your users in and authenticates every call. Your code never handles a password or a token for AGO, and keys for other services are kept as app secrets, outside the code.

<Note>
  AGO Apps is enabled per workspace. Contact AGO to turn it on. For now, only staff members of the workspace can open an app in their browser.
</Note>

This page describes what actually happens to your code, so that you, and the coding agent you work with, know what works and what does not.

## What you can build

* **An internal web page.** It is served at `https://<app>.<workspace>.agoapps.com`. Only members of your workspace can open it, after signing in with their AGO account.
* **An API for your agents.** An [HTTP Request Tool](/tools/http-request-tool) or an [API Data Source](/features/api-data-sources) that calls the app is authenticated automatically, and the app knows which user the agent is acting for.

An app is a single [Cloudflare Worker](https://developers.cloudflare.com/workers/): one `fetch` function that receives a request and returns a response. It has no database, and reads the keys of the services it calls from its [secrets](#secrets). Each request may use up to 30 seconds of CPU and call 1,000 other URLs. That covers calculators, formatters, lookups over external APIs, forms, dashboards that combine several APIs, document generation, and glue between your agents and the services they need.

## Create an app

### From Claude or any MCP client

1. [Connect your MCP client to AGO](/agent/connect-claude-web) and grant the **Apps Write** scope.
2. Ask, for example: *"Create an AGO app called `price-check` that returns the price including VAT for an amount and a country."*

Claude uses these tools:

| Tool | What it does |
| - | - |
| `create_app` | Creates the app and its git repository, then commits the starter files |
| `get_app_files` | Reads every file of the app |
| `commit_app_files` | Commits changes; every commit is built and deployed |
| `get_app` | Shows the URLs and recent deployments, with build errors and logs |
| `list_apps` | Lists the apps of the workspace |
| `get_app_git_credentials` | Returns a git remote and a write token valid one hour, for a regular git client |
| `list_app_secrets` | Lists the names of the app's [secrets](#secrets) |
| `delete_app_secret` | Deletes one of the app's secrets |

### From Claude Code, Codex or git

Get the remote and a token with `get_app_git_credentials`, then work like with any repository:

```bash theme={null}
git clone https://x:<token>@<remote host>/git/ago-apps/<workspace>--<app>.git
cd <workspace>--<app>
# edit, commit
git push origin main   # builds and deploys
```

### What a new app contains

| File | Purpose |
| - | - |
| `src/index.ts` | A working example: an HTML page at `/` and a JSON endpoint at `/api/hello` |
| `AGENTS.md` | The rules of the platform, read automatically by Codex and other coding agents |
| `CLAUDE.md` | Points Claude Code to `AGENTS.md` |

Keep `AGENTS.md` in the repository: it is what makes the coding agent write code that runs here.

## What happens when you commit

Every commit on `main` goes through these steps, usually in under a minute:

1. The commit is checked out in an isolated build container. It has no AGO or Cloudflare credentials.
2. If `package.json` exists, the dependencies are installed, without running install scripts (`postinstall` and others). With a committed `package-lock.json`, the exact versions it lists are installed; without one, the newest versions that `package.json` allows.
3. `src/index.ts`, or `src/index.js`, is bundled with esbuild into a single file, with every npm dependency included.
4. If `package.json` has a `test` script, `npm test` runs.
5. The bundle goes live, but only if its commit is still the latest one on `main`. It gets the next version number: 1, 2, 3…

If any step fails, nothing changes: the previous version keeps answering. If two commits are built at the same time and the older one finishes last, the older one is marked **superseded** and is not deployed.

Commits only build from `main`; other branches are stored but not deployed.

### Check which version is live

Every deployed app answers `GET /_ago/version`, before your own code runs:

```json theme={null}
{"version": 7, "commit": "3f2a9c…", "builtAt": "2026-10-18T12:58:10.656Z"}
```

`get_app` returns the same information as `live`, and **Admin › Developer › Apps** marks the deployment the app is serving. A change is live once the commit served equals the commit you made. Right after a deployment, some requests can still reach the previous version for a few seconds.

## How a request reaches your app

Every request first goes through AGO's gateway, which:

1. Checks who is calling. A browser needs an AGO session for this app; an agent call needs a token signed by AGO for this app.
2. Removes the `Authorization` header, AGO's session cookie and any `X-Ago-*` header sent by the client.
3. Adds the headers below, then passes the request to your code.

| Header | Sent when | Value |
| - | - | - |
| `X-Ago-Tenant` | always | your workspace name |
| `X-Ago-User-Email` | signed-in user, or agent acting for a known user | email address |
| `X-Ago-User-Name` | signed-in user | full name, URL-encoded: use `decodeURIComponent` |
| `X-Ago-User-Id` | signed-in user | AGO user id |
| `X-Ago-User-Staff` | signed-in user | `true` or `false` |
| `X-Ago-Caller` | call made by AGO (agent tool, API data source) | `ago` |

Because AGO already authenticated the request, your app must not ask for a password, check a token, or store credentials. Trust these headers: a client cannot forge them.

The first time someone opens the app, the browser goes to your AGO workspace and comes back signed in, without a consent screen. The session lasts one hour. After that, opening a page signs the user in again, usually without them noticing, while a `fetch` from the page receives `401` with `{"error": "login_required"}`: reload the page when that happens.

## Code reference

### The contract

```ts theme={null}
export default {
  async fetch(request: Request): Promise<Response> {
    return Response.json({ ok: true });
  },
};
```

The default export must have a `fetch` method that returns a `Response`. Route on `new URL(request.url).pathname` and `request.method`.

### What is available

* Web-standard APIs: `fetch`, `Request`, `Response`, `Headers`, `URL`, `URLSearchParams`, `crypto.subtle`, `crypto.randomUUID`, `TextEncoder`, `TextDecoder`, `atob`, `btoa`.
* npm packages that only use those APIs, for example `zod` or `date-fns`.

### What is not available

* Node.js built-ins: `fs`, `path`, `Buffer`, `process`, `child_process`, `net`. Packages that need them fail at build or at run time.
* A filesystem, a database or any storage. Global variables may survive between requests but can be reset at any time: never use them to keep data.
* Environment variables other than [secrets](#secrets).

### Secrets

Store the keys of the services your app calls, such as an OpenAI or a CRM API key, as secrets. Never put them in the code.

**Set a secret.** In **Developer** > **Apps**, open the app and add the secret in its **Secrets** tab, or call the API with the **Apps Write** scope:

```bash theme={null}
curl -X PUT "https://<your-tenant>.api.useago.com/api/v1/apps/<app>/secrets/OPENAI_API_KEY" \
  -H "X-API-Key: <your API key>" -H "Content-Type: application/json" \
  -d '{"value": "sk-..."}'
```

Setting a secret that already exists replaces its value. `GET /api/v1/apps/<app>/secrets` lists the names and `DELETE /api/v1/apps/<app>/secrets/<NAME>` deletes one. Claude can list and delete secrets, but cannot set them: give a key to AGO yourself, never in a conversation.

**Read a secret.** Secrets are the second argument of `fetch`:

```ts theme={null}
export default {
  async fetch(request: Request, env: { OPENAI_API_KEY?: string }): Promise<Response> {
    if (!env.OPENAI_API_KEY) return Response.json({ error: "OPENAI_API_KEY is not set" }, { status: 503 });
    const answer = await fetch("https://api.openai.com/v1/models", {
      headers: { Authorization: `Bearer ${env.OPENAI_API_KEY}` },
    });
    return Response.json({ status: answer.status });
  },
};
```

**Rules.**

* Names use uppercase letters, digits and underscores, start with a letter and have 64 characters at most. Names starting with `AGO_` are reserved.
* A value is at most 5 KB, and an app holds at most 50 secrets.
* An app receives secrets once its first build is deployed, which takes about 30 seconds after you create it.
* Values are never shown again, not even to staff: AGO only lists names. To change a value, set the secret again.
* Secrets are kept across deployments.
* Anyone who can change the app's code can read its secrets. Give the app its own key, with the lowest permissions and a spending limit when the service offers one.

### Limits

| Limit | Value |
| - | - |
| CPU time per request | 30 seconds (time spent waiting for `fetch` does not count) |
| Outgoing `fetch` calls per request | 1,000 |
| Bundle size | 10 MB |
| Build duration | 10 minutes |
| Files per commit through `commit_app_files` | 200, 2 MB in total |
| File size shown by `get_app_files` | 256 KB (larger files are listed without content) |

### Browser rules

* `POST`, `PUT`, `DELETE` and WebSocket requests must come from the app's own pages. A request sent from another site, including another app, gets `403` with `{"error": "cross_origin_request"}`.
* Cookies set by the app must be host-only. A `Set-Cookie` header with a `Domain` attribute is removed.

## Examples

### A JSON API with validation

```ts theme={null}
export default {
  async fetch(request: Request): Promise<Response> {
    const url = new URL(request.url);
    if (url.pathname !== "/vat" || request.method !== "GET") {
      return Response.json({ error: "not_found" }, { status: 404 });
    }
    const amount = Number(url.searchParams.get("amount"));
    const rates: Record<string, number> = { FR: 0.2, DE: 0.19, ES: 0.21 };
    const rate = rates[url.searchParams.get("country") ?? ""];
    if (!Number.isFinite(amount) || rate === undefined) {
      return Response.json({ error: "amount and a country among FR, DE, ES are required" }, { status: 400 });
    }
    return Response.json({ amount, rate, total: Math.round(amount * (1 + rate) * 100) / 100 });
  },
};
```

`GET /vat?amount=100&country=FR` returns `{"amount":100,"rate":0.2,"total":120}`.

### A page that posts to its own API

```ts theme={null}
const page = `<!doctype html><meta charset="utf-8">
<form id="f"><input name="note" required><button>Save</button></form><pre id="out"></pre>
<script>
  document.getElementById("f").onsubmit = async (event) => {
    event.preventDefault();
    const response = await fetch("/api/notes", { method: "POST", body: new FormData(event.target) });
    document.getElementById("out").textContent = JSON.stringify(await response.json());
  };
</script>`;

export default {
  async fetch(request: Request): Promise<Response> {
    const url = new URL(request.url);
    if (url.pathname === "/" && request.method === "GET") {
      return new Response(page, { headers: { "Content-Type": "text/html; charset=utf-8" } });
    }
    if (url.pathname === "/api/notes" && request.method === "POST") {
      const note = (await request.formData()).get("note");
      const author = request.headers.get("X-Ago-User-Email");
      return Response.json({ received: note, author });
    }
    return Response.json({ error: "not_found" }, { status: 404 });
  },
};
```

The `fetch` comes from the app's own page, so the gateway accepts the `POST`. The app has no storage: to keep notes, send them to a service that stores them, as in the next example.

### Calling an external API

```ts theme={null}
export default {
  async fetch(request: Request): Promise<Response> {
    const city = new URL(request.url).searchParams.get("city") ?? "Paris";
    const geo = await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(city)}&count=1`);
    const place = (await geo.json()).results?.[0];
    if (!place) return Response.json({ error: "unknown city" }, { status: 404 });
    const weather = await fetch(
      `https://api.open-meteo.com/v1/forecast?latitude=${place.latitude}&longitude=${place.longitude}&current=temperature_2m`,
    );
    return Response.json({ city: place.name, current: (await weather.json()).current });
  },
};
```

Each `fetch` counts toward the 1,000 calls per request. For an API that needs a key, store the key as a [secret](#secrets).

### Using an npm package

List the package in `package.json`; the build installs it.

```json theme={null}
{
  "name": "price-check",
  "private": true,
  "type": "module",
  "dependencies": { "zod": "^3.25.76" }
}
```

```ts theme={null}
import { z } from "zod";

const Query = z.object({ amount: z.coerce.number().positive(), country: z.enum(["FR", "DE", "ES"]) });

export default {
  async fetch(request: Request): Promise<Response> {
    const parsed = Query.safeParse(Object.fromEntries(new URL(request.url).searchParams));
    if (!parsed.success) return Response.json({ error: parsed.error.issues }, { status: 400 });
    return Response.json(parsed.data);
  },
};
```

The build bundles `zod` into the app.

<Tip>
  If you test the app on your computer, commit the `package-lock.json` that `npm install` creates: the build then installs exactly the versions you tested. Without it, a later build may pick up newer versions. Leave it out only if you edit the app without running npm, for example from Claude on the web.
</Tip>

To run tests at build time, add a `test` script and its dev dependencies to `package.json`.

### Calling your app from an agent

1. Open **Admin › Developer › Apps** and copy the app's **API endpoint**, for example `https://api.agoapps.com/acme/price-check`.
2. Create an [HTTP Request Tool](/tools/http-request-tool) whose URL is that endpoint followed by a path, such as `https://api.agoapps.com/acme/price-check/vat`, or use it in an [API Data Source](/features/api-data-sources).
3. Leave the authentication empty. AGO signs each call with a token valid 60 seconds for this app only, and adds `X-Ago-User-Email` when the agent acts for a user.

Only apps of your own workspace are signed this way.

## Monitor and troubleshoot

**Admin › Developer › Apps** lists every app with its web URL, its API endpoint and its last deployment. Open an app to find:

* **Overview**: what production runs (version, commit, deployment time, build time, size), the web URL and API endpoint with copy buttons, and the latest deployments. A banner flags a deployment that failed or is in progress.
* **Deployments**: the full history. Click a deployment to show its build logs; a failed one opens with its error.
* **Secrets**: the names of the app's [secrets](#secrets).
* **Code**: the files of the `main` branch.

**Visit** opens the web app in a new tab. The same information is available to Claude through `get_app`.

| Deployment status | Meaning |
| - | - |
| Deploying | The commit is being built |
| Deployed | This commit is live |
| Superseded | Built correctly, but a newer commit was already live: not deployed |
| Failed | The build or the tests failed; the previous version is still live. The error shows the end of the build output |

| Response | Meaning |
| - | - |
| `401 login_required` | The browser session is missing or expired: reload the page |
| `401 unauthorized` | A call to the API endpoint with an invalid or expired AGO token |
| `403` page from Cloudflare | A call to the API endpoint without any AGO token, for example from outside AGO: it is refused before reaching AGO Apps |
| `403 cross_origin_request` | A `POST`, `PUT`, `DELETE` or WebSocket request sent from another site |
| `404 not_found` | No app with this name in this workspace, or a path your code does not handle |
| `503 apps_disabled` | AGO Apps is turned off for the workspace |
| `503 auth_unavailable` | The gateway could not reach AGO to check the token; retry |
| `500` with `Worker exceeded CPU time limit` or `Too many subrequests` | The request went over one of the limits above |

## Security model

* Each app runs isolated from every other app, and the build container never holds credentials.
* The workspace an app belongs to comes from AGO's authentication, never from the request or the code.
* Users sign in through AGO; agent calls carry a token signed by AGO and valid 60 seconds for a single app. The app's code never sees either.
* Secret values are sent straight to the hosting platform and stored encrypted with the app. AGO does not keep them, and nobody can read them back through AGO.
* AGO can turn AGO Apps off for a workspace at any time; the apps then stop answering.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.