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

# Modal Reference

> Commands, customization, environment variables, and troubleshooting for the Modal template.

The Modal app is `agentos`, served as one always-warm container. Configuration lives in the `agentos-secrets` Modal secret, and the database is a Neon Postgres project named `agentos`.

## Manage

| Task | Command |
| - | - |
| Deploy code changes | `./scripts/modal/redeploy.sh` |
| Sync env variables | `./scripts/modal/env-sync.sh` (defaults to `.env.production`; pass `.env` to sync that instead) |
| Tail logs | `modal app logs agentos --follow` |
| List apps and status | `modal app list` |
| Tear down | `./scripts/modal/down.sh` |

> [!WARNING]
>
> `down.sh --yes` skips only the wrapper script's confirmation. It does not pass `--yes` to `modal app stop`, so Modal may still prompt while stopping the app.

> [!NOTE]
>
> `modal_app.py` pins `min_containers=1` and `max_containers=1`. The always-warm container keeps the in-process scheduler and MCP streams alive, and the cap stops two schedulers from double-firing every cron. Keep both settings.

## Production auth

Token-Based Authorization is on by default. Production startup requires `JWT_VERIFICATION_KEY` or a readable JWKS file at the container path in `JWT_JWKS_FILE`; otherwise the process exits.

Token-Based Auth gives you three things:

1. **No public access.** The server rejects requests without a valid token.
2. **Per-request identity.** Middleware validates the token and exposes its `user_id`, optional `session_id`, scopes, and claims to the request.
3. **Scope-based permissions.** Token scopes control access to AgentOS routes and resources.

The templates do not enable per-user data isolation. To scope non-admin session, memory, trace, and run access to the JWT subject, pass `authorization_config=AuthorizationConfig(user_isolation=True)` to `AgentOS`. See [User Isolation](https://agno-v2.mintlify.site/agent-os/security/authorization/user-isolation.md).

To opt out (not recommended), set `authorization=False` in `app/main.py` and redeploy. Use this only inside a private VPC behind another auth layer. Without it, anyone who guesses your modal.run URL can access your AgentOS backend.

## Customize

> **Add an agent**
>
> Ask your coding agent to run `/create-agent`, or do it by hand. Create `agents/my_agent.py`:
>
> ```python theme={null}
> from agno.agent import Agent
>
> from app.settings import default_model
> from db import get_postgres_db
>
> INSTRUCTIONS = """\
> What the agent does, which tools it uses, the rules to follow when answering.
> """
>
> my_agent = Agent(
>     id="my-agent",
>     name="My Agent",
>     model=default_model(),
>     db=get_postgres_db(),
>     instructions=INSTRUCTIONS,
>     enable_agentic_memory=True,
>     add_datetime_to_context=True,
>     add_history_to_context=True,
>     num_history_runs=5,
> )
> ```
>
> Register it in `app/main.py`:
>
> ```python theme={null}
> from agents.my_agent import my_agent
>
> agent_os = AgentOS(
>     ...
>     agents=[agent_builder, platform_manager, web_search, my_agent],
> )
> ```
>
> Add its UI metadata beneath the existing `manifest:` key in `app/config.yaml`:
>
> ```yaml theme={null}
>   my-agent:
>     description: "What the agent does."
>     quick_prompts:
>       - "First example prompt"
>       - "Second example prompt"
>       - "Third example prompt"
> ```
>
> Local containers hot-reload on save. For production, run `./scripts/modal/redeploy.sh`.

> **Change the model**
>
> `app/settings.py` defines `default_model()`, used by every agent. Change it in one place:
>
> ```python theme={null}
> from agno.models.anthropic import Claude
>
> def default_model():
>     return Claude(id="claude-sonnet-5")
> ```
>
> Add `anthropic` to `pyproject.toml`, set the provider key in your env, and regenerate pins:
>
> ```bash theme={null}
> ./scripts/generate_requirements.sh
> ```
>
> Rebuild locally with `docker compose up -d --build`. For production:
>
> ```bash theme={null}
> ./scripts/modal/env-sync.sh
> ```
>
> `env-sync.sh` rewrites the secret with the new provider key and redeploys. The redeploy rebuilds the image, so the new dependency ships with it.

> **Add tools**
>
> Agno ships 100+ toolkits. See [Toolkits](https://agno-v2.mintlify.site/tools/toolkits/overview.md).
>
> ```python theme={null}
> from agno.tools.slack import SlackTools
>
> my_agent = Agent(
>     ...
>     tools=[SlackTools()],
> )
> ```

> **Add dependencies**
>
> 1. Edit `pyproject.toml`.
> 2. Regenerate pins: `./scripts/generate_requirements.sh` (add `upgrade` to refresh every pin).
> 3. Rebuild locally with `docker compose up -d --build`, or redeploy with `./scripts/modal/redeploy.sh`.

> **Enable Slack**
>
> Set both variables in your env file:
>
> ```bash theme={null}
> SLACK_BOT_TOKEN=xoxb-...
> SLACK_SIGNING_SECRET=...
> ```
>
> Sync with `./scripts/modal/env-sync.sh`. The interface activates automatically and routes messages to Agent Builder; change the `agent=` argument in `app/main.py` to point at another agent. See [Slack setup](https://agno-v2.mintlify.site/agent-os/interfaces/slack/setup.md).

> **Toggle scheduled workflows**
>
> The deployment check runs daily by default (`ENABLE_DEPLOY_CHECK=True`); it is deterministic and free. The `run-evals` schedule is always registered but starts disabled because it uses model calls. Enable it from the AgentOS UI. Both workflows remain runnable on demand.

## Format, validate, and run evals

The format, validate, and eval scripts run on the host and need a venv. Set it up once:

```bash theme={null}
./scripts/venv_setup.sh
source .venv/bin/activate
```

| Task | Command |
| - | - |
| Format | `./scripts/format.sh` |
| Lint and type-check | `./scripts/validate.sh` |
| Run smoke evals | `python -m evals --tag smoke` |

`./scripts/mcp_check.sh` runs inside the container, so it needs no venv.

## Environment variables

| Variable | Required | Default | Description |
| - | - | - | - |
| `OPENAI_API_KEY` | Yes | - | Models and embeddings. |
| `RUNTIME_ENV` | No | `prd` | `dev` disables JWT. Compose sets it for local. Never put it in an env file that syncs to Modal, or production deploys unauthenticated. |
| `JWT_VERIFICATION_KEY` | Production | - | Public key from os.agno.com. Quote the value so the multi-line PEM parses as one variable. |
| `JWT_JWKS_FILE` | Production | - | Path inside the Modal container to a JWKS file. The scripts put only this path into `agentos-secrets`. Put the file in the Docker build context or add an explicit Modal mount before deploying. |
| `MCP_CONNECT_SECRET` | No | generated by `up.sh` | OAuth consent secret (16+ chars) for connecting claude.ai and ChatGPT to `/mcp`. `up.sh` generates one on deploy and writes it to `.env.production`. |
| `AGENTOS_MCP_SIGNING_KEY` | No | generated | Optional high-entropy signing-key material (32+ chars) for OAuth tokens. Unset, a strong key is generated and persisted in the database. Rotating it invalidates outstanding tokens. |
| `AGENTOS_URL` | No | `http://127.0.0.1:8000` | Scheduler base URL. `up.sh` sets it to your modal.run URL. Scheduled jobs never fire if it stays at the default in production. When `MCP_CONNECT_SECRET` is set, OAuth metadata also derives its public origin from this URL. |
| `ENABLE_DEPLOY_CHECK` | No | `True` | Daily deployment-check cron. |
| `EVALS_TAG` | No | `smoke` | Eval tag the run-evals workflow runs. |
| `EVALS_CASE_TIMEOUT_SECONDS` | No | `90` | Per-case timeout for run-evals runs. |
| `EVALS_SUITE_TIMEOUT_SECONDS` | No | `900` | Whole-suite timeout for run-evals runs. |
| `PARALLEL_API_KEY` | No | - | WebSearch uses the Parallel SDK when set, keyless MCP otherwise. |
| `SLACK_BOT_TOKEN` | No | - | Set with the signing secret to enable Slack. |
| `SLACK_SIGNING_SECRET` | No | - | Set with the bot token to enable Slack. |
| `DB_HOST` / `DB_PORT` / `DB_USER` / `DB_PASS` / `DB_DATABASE` | No | matches compose | Postgres connection. `up.sh` fills these from your Neon project. |
| `DB_DRIVER` | No | `postgresql+psycopg` | SQLAlchemy driver. |
| `NEON_PROJECT_ID` | No | written by `up.sh` | Identifies the Neon project so `down.sh` can delete it. `env-sync.sh` skips NEON\_\* keys; they never sync to the app. |
| `NEON_ORG_ID` | No | - | Neon organization for unattended deploys. `neonctl projects create` prompts for an org and hangs non-interactive runs; set it (find yours with `neonctl orgs list`) so `up.sh` can pass `--org-id`. |
| `PGSSLMODE` | No | - | The deploy scripts set it to `require` in the Modal secret. Neon requires TLS, and libpq honors the variable, so the app needs no change. |
| `AGNO_DEBUG` | No | `False` | Verbose Agno logs. Compose sets it for dev. |
| `WAIT_FOR_DB` | No | `False` | If `True`, the entrypoint blocks on the database before starting. Compose sets it. |

## Troubleshooting

> **modal: command not found**
>
> Install the CLI with `pip install modal` or `uv tool install modal`, then run `modal token new`.

> **neonctl: command not found**
>
> Install it with `brew install neonctl` or `npm i -g neonctl`, then run `neonctl auth`.

> **up.sh stops at a Neon organization prompt**
>
> Neon projects are org-scoped, so `neonctl projects create` asks which organization to use and hangs non-interactive runs. Set `NEON_ORG_ID` in `.env.production` (find yours with `neonctl orgs list`) and re-run `./scripts/modal/up.sh`; the script passes it as `--org-id` so the deploy runs unattended.

> **up.sh pauses asking for a JWT key**
>
> Expected. At [os.agno.com](https://os.agno.com), choose **Connect OS** → **Live**, enter your modal.run URL, name it **Live AgentOS**, turn on **Token-Based Authorization (JWT)** on the connection panel, and connect. The UI generates the public key. If the OS is already connected, enable the setting under **Settings** → **OS & Security**. Paste the full PEM into the script prompt. To add a PEM later, set `JWT_VERIFICATION_KEY` and run `./scripts/modal/env-sync.sh`. To use JWKS, add the file to the Docker build context or configure a Modal mount, set `JWT_JWKS_FILE` to its container path, then deploy.

> **App fails to start in production**
>
> JWT auth is on whenever `RUNTIME_ENV` is not `dev`. Set `JWT_VERIFICATION_KEY` and sync. For `JWT_JWKS_FILE`, first make the file available inside the Modal image or through a mount, then set its container path and sync. To opt out inside a private VPC behind another auth layer, set `authorization=False` in `app/main.py`.

> **Env changes don't take effect**
>
> Secrets are read at container start, so rewriting the secret alone changes nothing. `./scripts/modal/env-sync.sh` does both steps: it rewrites `agentos-secrets` and redeploys to roll the container.

> **Scheduled jobs never fire**
>
> `AGENTOS_URL` is still the localhost default. `up.sh` sets it to your modal.run URL automatically; for a custom domain or tunnel, set it by hand and run `./scripts/modal/env-sync.sh`.

> **up.sh says "Reusing database" after a teardown**
>
> `down.sh` deletes the Neon project but leaves `NEON_PROJECT_ID` and the `DB_*` values in your env file, so `up.sh` thinks a database still exists. Delete those lines and re-run `./scripts/modal/up.sh` to provision a fresh one.

> **down.sh reports "Teardown incomplete"**
>
> The script only declares success once the app no longer shows as running in `modal app list` and the project is gone from `neonctl projects list`. Check both, then re-run it or finish by hand: `modal app stop agentos` and `neonctl projects delete <project-id>`.


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