Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Custom Runtime Apps

By default, a deployed agent uses Amodal's shared chat shell. A repo can instead ship its own single-page app and have Amodal serve that app from the agent's URL.

Minimal Manifest

Enable the custom app in amodal.json:

{
  "name": "investor-relations",
  "version": "0.1.0",
  "runtimeApp": {
    "custom": true
  }
}

Optional fields:

{
  "runtimeApp": {
    "custom": true,
    "build": "npm run build",
    "dist": "dist"
  }
}
FieldDefaultDescription
customrequiredSet to true to build and serve the repo's SPA.
buildnpm run buildBuild command run from the repo root after dependencies install.
distdistDirectory uploaded as the SPA artifact.

Expected Repo Shape

my-agent/
├── amodal.json
├── package.json
├── index.html
├── vite.config.ts
├── src/
│   └── main.tsx
└── amodal/
    ├── stores/
    ├── skills/
    ├── tools/
    └── connections/

The app should use same-origin API calls. In browser code, derive the runtime base from window.location.origin; only use VITE_RUNTIME_URL for local development against a remote runtime. Requests such as /chat/stream and /api/stores/investors then hit the agent's own origin through the edge worker.

A runtime URL that was undefined when the request was built produces undefined/chat/stream, which the agent origin answers with the app's HTML. @amodalai/react rejects such a URL, and any stream response that is not text/event-stream, with an SSEStreamError naming the URL, the status, and the content type, so the chat reports the failure instead of falling silent.

Build And Serve Flow

When runtimeApp.custom is enabled:

  1. The build server clones the connected Git repository or downloads the Amodal-hosted source snapshot.
  2. It installs dependencies, using npm ci when a usable lockfile is present and npm install otherwise.
  3. It loads and validates the agent definition.
  4. It uploads server artifacts to R2.
  5. It runs the SPA build command.
  6. It uploads the output directory (dist by default) to R2 under runtime-app/{agentId}/{deployId}/spa.
  7. It packages and uploads repo.tar.gz to R2 for the runtime to load the deployed source.

Amodal serves the selected deploy's app from this artifact. Browser routes return index.html; asset routes return the requested file.

Failure Behavior

If the custom app build fails or its output directory is missing, the deploy fails. Inspect the build logs, fix the source, and redeploy.

If runtimeApp.custom is omitted or false, the deploy uses the shared default runtime app artifact.

Iteration Loop

Build and test the SPA locally with the same command listed in runtimeApp.build. For GitHub-connected source, commit the frontend source and push to the connected branch. For Amodal-hosted source, save the source changes as a version in Amodal. Deploy the saved source through Amodal or the Platform API; GitHub-connected agents can also deploy through CI.

When a deploy fails, inspect build status and logs:

curl "$AMODAL_API_BASE/api/builds/$AMODAL_BUILD_ID/status" \
  -H "Authorization: Bearer $AMODAL_API_KEY"
 
curl "$AMODAL_API_BASE/api/builds/$AMODAL_BUILD_ID/logs" \
  -H "Authorization: Bearer $AMODAL_API_KEY"

Fix the source and redeploy. Do not patch the uploaded SPA artifact directly; the connected repository or Amodal-hosted version remains the source of truth.

SDK Use

Use @amodalai/react for runtime APIs, sessions, events, and chat. See the React SDK for store-hook support and scope limitations.

Minimal Wiring

Install the SDK and React:

npm install @amodalai/react react react-dom

Import the stylesheet, wrap the app in AmodalProvider, and render AmodalChat. Use an absolute runtime URL. For the default team-only hosted login, omit getToken; the edge authenticates same-origin requests with the session cookie.

// src/main.tsx
import { createRoot } from 'react-dom/client';
import '@amodalai/react/style.css';
import { App } from './App';
 
createRoot(document.getElementById('root')!).render(<App />);
// src/App.tsx
import { AmodalProvider, AmodalChat } from '@amodalai/react';
 
const runtimeUrl = window.location.origin;
 
export function App() {
  return (
    <AmodalProvider runtimeUrl={runtimeUrl}>
      <AmodalChat serverUrl={runtimeUrl} />
    </AmodalProvider>
  );
}

Wiring Agent Capabilities

The chat loop uses the deployed agent definition. To limit the resources available to default chat, declare an agent under agents/default/:

agents/default/agent.json:

{
  "name": "Default",
  "skills": ["Craft Greeting"],
  "connections": ["advice"],
  "stores": {
    "visitors": "rw"
  },
  "tools": ["shout"]
}

The directory requires a sibling AGENT.md prompt. Without it, the directory is ignored. Resource names must match the loaded definitions.

When no default agent is declared, default chat uses the bundle's available resources. agents/main/AGENT.md supplies the base prompt, and its agent.json can opt into additional built-in tools; it does not restrict the skill, connection, or store list. See Agents for resource selection.

Authenticating Chat From a Custom App

Chat requires runtime authentication even when the app is public.

  • Hosted team login: use same-origin calls and omit getToken with the default platform verifier.
  • Product login: for multi-tenant access, point the SDK at an application proxy that retains runtime credentials and authorizes each scope, session, and operation. Return your application token through getToken. Pass that callback to both AmodalProvider and AmodalChat; the chat component only inherits the provider's URL.
  • Public app: keep persistent keys on a backend. Public app access does not authorize chat or mint a token.

See Authentication for hosted JWKS federation and Embedding for scoped product integration.