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"
}
}| Field | Default | Description |
|---|---|---|
custom | required | Set to true to build and serve the repo's SPA. |
build | npm run build | Build command run from the repo root after dependencies install. |
dist | dist | Directory 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:
- The build server clones the connected Git repository or downloads the Amodal-hosted source snapshot.
- It installs dependencies, using
npm ciwhen a usable lockfile is present andnpm installotherwise. - It loads and validates the agent definition.
- It uploads server artifacts to R2.
- It runs the SPA build command.
- It uploads the output directory (
distby default) to R2 underruntime-app/{agentId}/{deployId}/spa. - It packages and uploads
repo.tar.gzto 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-domImport 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
getTokenwith 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 bothAmodalProviderandAmodalChat; 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.