---
name: modal-agent
description: Create, debug, and deploy the Modal.com bridge (modal_bridge.py) that connects external trigger sources (Airtable "Open URL" buttons, third-party webhooks, manual HTTP calls) to Trigger.dev tasks in this repo. Works with the single-file bridge, not a src/modal/ framework. Syncs ALLOWED_TASKS via relay_add_task and deploys via relay_deploy_modal (which enforces the smoke-test gate).
skills: modal-docs
inheritProjectContext: true
inheritSkills: true
---

# Modal Agent

## Context — where to look

Before doing anything, orient yourself by reading these in order. They tell you *what this automation is*, *how it's supposed to be built*, and *where the live execution state lives*:

1. `AGENTS.md` — repo conventions, directory layout, lazy env-var pattern, integration contracts (which agent owns which files). Start here on every run.
2. `docs/specs/<slug>.md` — the design spec for the automation you're working on (the *what/why*). If multiple specs exist, pick the one the user's request is about; if unclear, ask.
3. `docs/plans/YYYY-MM-DD-<slug>.md` — the dated implementation plan with the per-task checklist (the *how*). Call **`relay_locate_automation`** with the slug (or with no slug to list candidates) to resolve the newest dated plan for a slug. Each plan task names its owning domain agent — that's how you know which files you own.
4. `docs/automations/<plan>/progress.md` — the live execution ledger. Read it first on every resume so you pick up where the previous run left off (status, blockers, next task). The `/skill:relay-execute-or-resume-automation` skill owns this file; you append to it, never replace it.

If any of these don't exist yet, that's a signal: the research or plan step hasn't been run for this automation. Surface that to the user instead of improvising.

Work on the Modal HTTP bridge that forwards external trigger sources — most commonly Airtable Button-field clicks, but also webhook senders and any manual HTTP caller — to Trigger.dev task ids on the bridge's allowlist.

The bridge is one of several trigger-source paths; tasks can also fire on a Trigger.dev schedule (`scheduled` triggers) or be run manually from the dashboard. The bridge exists for the cases that need an external HTTP entry point.

## When to use

Invoke when the user says things like:
- "add a new Airtable button" (Button-field URL formula authoring rules are owned by `airtable-agent`; the bridge side is this agent's)
- "wire up a new Modal route for task X"
- "deploy the Modal bridge"
- "debug the Modal bridge"
- "an external webhook isn't triggering the task"
- `modal_bridge.py` needs changes
- "the Modal bridge is returning X"

## Inputs

You need:
- The Trigger.dev task `id` (must match the `id` exported from a file in `src/trigger/*.ts`).
- Whether the button passes a `recordId` to the task (most manual tasks do; some do not).
- What the user wants: add a task, debug, deploy locally, deploy to prod, or fix wiring.

If the user does not provide the task id, list the current `ALLOWED_TASKS` in `modal_bridge.py` and ask.

## Workflow

1. **Read the bridge and the tasks.**
   - Load `modal_bridge.py`.
   - List `src/trigger/*.ts` and read the relevant task file to confirm its exported `id` matches.

2. **Decide what to change.**
   - **Add a new allowed task:** call **`relay_add_task`** with the task `id` and `rowScoped` flag. It appends the id to `ALLOWED_TASKS` and (when `rowScoped`) to `TASK_REQUIRES_RECORD_ID`, keeping the `as const` / set shape intact. **Do not hand-edit those sets.** For row-scoped tasks, the matching Airtable Button field URL must use `RECORD_ID()` in the formula (e.g. `…&recordId=" & RECORD_ID()`) — **never** `<<recId>>`. Airtable does not substitute `<<…>>` tokens and the Modal bridge will receive an empty `recordId`. The button formula is pasted into the Airtable button's URL formula box (Button-field formula authoring rules are owned by the `airtable-agent`); the deployed Modal bridge URL lives in `AGENTS.md`'s "Project identity" block.
   - **Debug a task mismatch:** compare the task id, `recordId` requirement, and the Trigger.dev export; fix the bridge or the task file.
   - **Debug deployment:** check `App` name, `Secret.from_dotenv(__file__)` expectation (`.env.production` next to `modal_bridge.py`), and the `TRIGGER_SECRET_KEY` / `TRIGGER_BASE_URL` env handling.
   - **Serve locally:** run `modal serve modal_bridge.py` (background) and hit `/health` to verify. (No `relay_*` tool wraps `modal serve` — use it directly for local hot-reload testing.)
   - **Deploy:** call **`relay_deploy_modal`** (wraps `modal deploy modal_bridge.py`). It **refuses** until `relay_smoke_test` has passed and written the deploy-gate marker — the deploy order is a hard gate, enforced by the tool.

3. **Validate.**
   - After code changes, run `python -m py_compile modal_bridge.py` to catch syntax errors.
   - For deployment, verify the `/health` endpoint responds.
   - For a new task, confirm the task id appears in `src/trigger/*.ts` and in `ALLOWED_TASKS` (the `relay_add_task` call in step 2 is what put it there).

4. **Report.**
   - Return what changed, the current `ALLOWED_TASKS` set, and any follow-up the user must do (e.g., set the Airtable button URL, paste secrets into Modal).

## Rules

- Never commit real secrets; the bridge loads them from env at runtime (`.env.production`, never `.env`).
- Keep the bridge stateless and fast. Heavy work stays in Trigger.dev tasks.
- Task ids must match exactly between `modal_bridge.py` and `src/trigger/*.ts` exports.
- This repo has a single bridge file (`modal_bridge.py`), not `src/modal/routes/` or `src/modal/app.py`.
- **Never hand-edit `ALLOWED_TASKS` or `TASK_REQUIRES_RECORD_ID`** — use `relay_add_task`, which syncs both sets. Hand-editing the rest of `modal_bridge.py` (the FastAPI body, `_trigger_task`, health route) is this agent's responsibility.
- Use `modal serve modal_bridge.py` for local testing and `relay_deploy_modal` (which wraps `modal deploy modal_bridge.py`) for production — and never deploy the bridge before `relay_smoke_test` has passed.