Skip to main content
Synth’s CLI can deploy any TaskAppConfig to Modal. This page captures what the tooling expects so you can ship a working Modal app on the first try.

Required configuration objects

ModalTaskAppConfig

Instances of ModalTaskAppConfig (defined in synth_ai/task_app_cfgs.py) describe how to call the Modal CLI:
{
    "task_app_path": Path,
    "modal_app_path": Path,
    "modal_bin_path": Path,
    "cmd_arg": "deploy" | "serve",
    "task_app_name": str | null,
    "dry_run": bool
}
The CLI (synth_ai/cli/deploy.py and synth_ai/cli/task_apps.py) validates these fields before dispatching to deploy_modal_app.

TaskAppEntry.modal

When registering a task app, set TaskAppEntry.modal to a ModalDeploymentConfig so discoverable apps automatically inherit default resources (CPU, memory, mounts, secrets) (synth_ai/task/apps/__init__.py). deploy_modal_app loads your Modal script and enforces the following invariants (synth_ai/utils/modal.py:134):
  • The file must define modal.App(...) (or expose modal_app). If modal.App cannot be found, deployment aborts (ensure_py_file_defines_modal_app).
  • The module must expose an ASGI entrypoint decorated with @modal.asgi_app() that returns the FastAPI object produced by create_task_app(...). Typical pattern:
app = modal.App("my-task-app")

@app.function(image=image, mounts=[...], secrets=[...])
@modal.asgi_app()
def fastapi_app():
    from synth_ai.task.server import create_task_app
    from my_package.task_app import build_config
    return create_task_app(build_config())
  • When the deploy helper loads your module it automatically:
    • Mounts the Modal script’s directory and the repo root into /root/... so intra-repo imports resolve (synth_ai/utils/modal.py:162).
    • Attempts to wrap image = modal.Image(...) definitions with add_local_dir for the same mounts (synth_ai/utils/modal.py:171).
    • Injects an inline ENVIRONMENT_API_KEY secret onto each function decorator when an inline secret is supported (synth_ai/utils/modal.py:200).

Mount your task app

Add a modal.Mount for the task app module (or directory) so Modal sees the same files the CLI uses locally. Without an explicit mount the container only receives the Modal script, and imports such as from task_app import config fail.
task_app_mount = modal.Mount.from_local_dir(
    local_path=".",               # or point at the task app folder
    remote_path="/root",
    condition=lambda p: p.endswith(".py")
)

@app.function(
    image=image,
    mounts=[task_app_mount],
    secrets=[modal.Secret.from_dotenv()],
)
@modal.asgi_app()
def fastapi_app():
    from task_app import build_config   # your task app factory
    from synth_ai.task.server import create_task_app
    return create_task_app(build_config())
Mount individual files (modal.Mount.from_local_file(...)) when you prefer a tighter scope; the important part is ensuring the task app code is listed in mounts=[...].

Deployment flow

  1. Invoke synth-ai task-app deploy <APP_ID> --runtime modal (or uvx synth-ai deploy --runtime modal). The CLI:
    • Resolves the task app entry and its ModalTaskAppConfig.
    • Locates the Modal CLI (modal) unless you override --modal-cli.
    • Loads optional .env files, ensuring ENVIRONMENT_API_KEY is present.
    • Generates a temporary wrapper module that imports your Modal script, adds repo mounts, and exposes the resolved app.
  2. The wrapper calls the Modal CLI with modal <cmd_arg> <wrapper_path> [--name ...].
  3. All stdout is streamed to the terminal. When Modal prints a *.modal.run URL, the helper writes it to .env as TASK_APP_URL (synth_ai/utils/modal.py:218).

Inline secrets and environment variables

  • Inline secret injection is best-effort. If your Modal function already lists secrets, the helper appends Secret.from_dict({"ENVIRONMENT_API_KEY": ...}) when possible.
  • For long-lived deployments, create a Modal Secret (modal secret create ...) and reference it directly in your app script; the helper won’t override existing secrets=[...] definitions.
  • Use Modal volume mounts or extra_local_dirs in ModalDeploymentConfig when you need additional files beyond the repo snapshot.

Local testing tips

  • modal serve <modal_app.py> runs the ASGI app locally via Modal. Ensure your script installs synth-ai in editable mode inside the Modal container if the app relies on the repo (see examples/task_apps/pokemon_battle/modal_app.py).
  • If you see import errors, verify the repo path is mounted and pip install -e is executed inside the Modal function (the example does this before calling create_task_app).
With these pieces in place, synth-ai task-app deploy --runtime modal can package, deploy, and record the Modal URL for any registered task app.