skills/tools-and-apis/policyengine-modal-deployment-skill/SKILL.md
Deploying PolicyEngine backend APIs to Modal — workspace setup, authentication, deployment commands, environments, and troubleshooting
npx skillsauth add policyengine/policyengine-claude policyengine-modal-deploymentInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
How to deploy PolicyEngine backend APIs (custom Modal backends for dashboards and interactive tools) to Modal under the PolicyEngine organizational workspace.
This skill applies only when a dashboard or tool uses the custom-backend data pattern. If the project uses api-v2-alpha (stub data or direct API calls), no Modal deployment is needed.
PolicyEngine uses a shared Modal workspace called policyengine. All backend deployments MUST target this workspace — never a personal workspace.
The policyengine workspace has three environments:
| Environment | Web suffix | URL pattern | Purpose |
|-------------|-----------|-------------|---------|
| main | (empty) | policyengine--<app>-<func>.modal.run | Production |
| staging | staging | policyengine-staging--<app>-<func>.modal.run | Pre-production testing |
| testing | testing | policyengine-testing--<app>-<func>.modal.run | Development/CI |
Default to main for production deployments.
pip install modalpolicyengine workspace stored in a local profile# Create a token for the policyengine workspace (opens browser)
modal token new --profile policyengine
# Activate the profile
modal profile activate policyengine
# Verify — must show "Workspace: policyengine"
modal token info
HUMAN GATE: Before any deployment, verify the active workspace:
modal token info
modal profile list
Expected output from modal profile list:
┏━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
┃ ┃ Profile ┃ Workspace ┃
┡━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━┩
│ • │ policyengine │ policyengine │
└───┴──────────────┴──────────────┘
The • indicates the active profile. If policyengine is not active or not present:
Authentication required. Your Modal CLI is not configured for the
policyengineworkspace.Run:
modal token new --profile policyengine modal profile activate policyengineIf you don't have access to the PolicyEngine workspace, ask a workspace owner to invite you at https://modal.com/settings/policyengine
Do NOT proceed with deployment until modal token info shows Workspace: policyengine.
Modal CLI respects MODAL_TOKEN_ID and MODAL_TOKEN_SECRET environment variables, which override the profile. If these are set (e.g., from a CI environment), the CLI will deploy to whatever workspace those tokens belong to — potentially a personal workspace.
Always unset before deploying:
unset MODAL_TOKEN_ID MODAL_TOKEN_SECRET
The Modal app name MUST match the dashboard/tool repo name in kebab-case:
app = modal.App("my-dashboard-name")
This keeps the app name consistent with the GitHub repo and Vercel project.
Build a container image with the required Python packages:
image = (
modal.Image.debian_slim(python_version="3.12")
.pip_install(
"policyengine-us>=1.155.0", # Pin minimum version
"fastapi[standard]",
"numpy",
"pandas",
)
.env({"NUMEXPR_MAX_THREADS": "4"})
.add_local_dir("api", "/root/api") # Local source code
.add_local_file("config.yaml", "/root/config.yaml") # Config files
)
Guidelines:
debian_slim with Python 3.12 or 3.13policyengine-us / policyengine-uk.add_local_dir() / .add_local_file() for project source code.env() for non-secret environment variablesFor tools with one calculation endpoint:
@app.function(image=image, timeout=300, memory=2048)
@modal.web_endpoint(method="POST")
def calculate(data: dict) -> dict:
from policyengine_us import Simulation
# Build simulation from data, return results
return {"result": value}
URL: https://policyengine--<app-name>-calculate.modal.run
For dashboards with multiple API routes:
@app.function(image=image, timeout=300, memory=2048)
@modal.concurrent(max_inputs=100)
@modal.asgi_app()
def fastapi_app():
from api.main import app as api
return api
URL: https://policyengine--<app-name>-fastapi-app.modal.run
Every Modal backend SHOULD include a health check:
@app.function(image=image)
@modal.web_endpoint(method="GET")
def health():
return {"status": "ok"}
| Parameter | Default | Recommended for PE | Purpose |
|-----------|---------|-------------------|---------|
| timeout | 60s | 300 | PolicyEngine simulations can take minutes |
| memory | 128MB | 2048 | PE models are memory-intensive |
| @modal.concurrent(max_inputs=N) | 1 | 100 | Handle concurrent requests without cold starts |
Store sensitive values as Modal Secrets (not in code or .env files):
# Create a secret
modal secret create my-secret API_KEY=abc123
# List secrets
modal secret list
Reference in code:
@app.function(
image=image,
secrets=[modal.Secret.from_name("my-secret")],
)
def my_function():
import os
api_key = os.environ["API_KEY"] # Injected by Modal
Existing secrets in the policyengine workspace:
policyengine-logfire — logging/observabilitygcp-credentials — Google Cloud accesshuggingface-token — HuggingFace model accessanthropic-api-key — Anthropic API access# 1. Ensure correct workspace
unset MODAL_TOKEN_ID MODAL_TOKEN_SECRET
modal token info # Verify "Workspace: policyengine"
# 2. Deploy to production
modal deploy modal_app.py --env main
# 3. Deploy to staging (for testing)
modal deploy modal_app.py --env staging
Flags:
--env main / --env staging / --env testing — target environment--name TEXT — override the deployment name (rarely needed)--tag TEXT — tag the deployment with a version string--stream-logs — stream container logs during deployment# List deployed apps
modal app list --env main
# Health check
curl -s -w "\n%{http_code}" https://policyengine--DASHBOARD_NAME-health.modal.run
# Test the endpoint
curl -X POST https://policyengine--DASHBOARD_NAME-calculate.modal.run \
-H "Content-Type: application/json" \
-d '{"test": true}'
After Modal deployment, set the API URL as an environment variable in the Vercel project:
vercel env add NEXT_PUBLIC_API_URL production
# Enter: https://policyengine--DASHBOARD_NAME-calculate.modal.run
vercel --prod --force --yes --scope policy-engine
The --force flag is required to rebuild with the new environment variable.
Redeploying an existing app is the same command — Modal handles zero-downtime transitions:
modal deploy modal_app.py --env main
WARNING: This is destructive and irreversible. A stopped app cannot be restarted; you must redeploy.
modal app stop <app-name>
# View logs for a deployed app
modal app logs <app-name>
# List all apps and their status
modal app list --env main
App states:
deployed — running and accepting requestsephemeral — temporary (from modal serve)stopped — permanently stopped| Issue | Cause | Fix |
|-------|-------|-----|
| modal token info shows wrong workspace | Wrong profile active | modal profile activate policyengine |
| Deploy goes to personal workspace | MODAL_TOKEN_ID env var set | unset MODAL_TOKEN_ID MODAL_TOKEN_SECRET |
| 404 on endpoint URL | App stopped or never deployed | modal deploy modal_app.py --env main |
| Cold start latency (5-15s) | No warm containers | Add @modal.concurrent(max_inputs=100) |
| MemoryError during simulation | Container memory too low | Increase memory= (try 4096) |
| Timeout error | Simulation exceeds limit | Increase timeout= (try 600) |
| Python dependency conflict | Version mismatch | Pin exact versions in .pip_install() |
| Missing local files in container | Forgot .add_local_dir() | Add source dirs to image definition |
| Modal app silently disappeared | Unknown — can happen | curl the URL; if 404, redeploy |
policyengine workspaceMODAL_TOKEN_ID / MODAL_TOKEN_SECRET env vars set when deploying via profilemodal run for production deployments — use modal deploymodal serve for production — it creates ephemeral apps that stop when you close your terminalmodal token infotools
ONLY use this skill when users explicitly ask about the PolicyEngine Python package installation, REST API endpoints, API authentication, rate limits, or policyengine.py client library. DO NOT use for household benefit/tax calculations — ALWAYS use policyengine-us or policyengine-uk instead. This skill is about the API/client tooling itself, not about calculating benefits or taxes.
development
ALWAYS USE THIS SKILL for PolicyEngine microsimulation, population-level analysis, winners/losers calculations. Triggers: microsimulation, share who would lose/gain, policy impact, national average, weighted analysis, cost, revenue impact, budgetary, estimate the cost, federal revenues, tax revenue, budget score, how much would it cost, how much would the policy cost, total cost of, aggregate impact, cost to the government, revenue loss, fiscal impact, poverty impact, child poverty, deep poverty, poverty rate, poverty reduction, how many people lifted out of poverty, SPM poverty, distributional impact, state tax, state-level, California, New York, UBI, universal basic income, flat tax, standard deduction, winners and losers, winners, losers, inequality, Gini, decile, SALT, marginal tax rate, effective tax rate. NOT for single-household calculations like "what would my benefit be" - use policyengine-us or policyengine-uk for those. Use this skill's code pattern; explore codebase for parameter paths if needed.
development
PolicyEngine API v2 - Next-generation microservices architecture with monorepo structure
development
ALWAYS LOAD THIS SKILL before setting up any Python environment or installing packages. Defines the standard: uv, Python 3.13, uv pip install, .venv at project root. Triggers: "set up python", "install python", "create a venv", "virtual environment", "pip install", "install packages", "uv pip", "uv venv", "python version", "VIRTUAL_ENV", "venv conflict", "which python", "activate", "deactivate", "run the script", "run with uv", "uv run", "pyproject.toml", "install dependencies", "install requirements", "install the package", "editable install", "pip install -e", "latest package", "latest version", "current version", "newest version".