一键导入
modal-remote-functions
Use when calling deployed Modal functions from a CLI or external service without needing the Modal app running locally
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Use when calling deployed Modal functions from a CLI or external service without needing the Modal app running locally
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Scan design/ for backlog/todo artifacts, check if work has been merged into the codebase, and auto-rename completed ones to done-. Use for periodic hygiene, after merging branches, or to audit design artifact status.
Use when writing Dagger pipelines in Python, building containerized CI/CD with the Dagger Python SDK, creating Dagger modules/functions/objects, integrating services (PostgreSQL, Redis) in Dagger, using Dagger LLM/agent features, or running Dagger in GitHub Actions.
Use when creating specifications, implementation plans, or tools for a feature - enforces consistent naming across related artifacts
Use when you have a written implementation plan to execute in a separate session with review checkpoints
Use this skill when the user wants to run hotdata CLI commands, query the Hotdata API, list workspaces, list connections, create connections, list tables, manage datasets, execute SQL queries, inspect query run history, search tables, manage indexes, manage sandboxes, manage workspace context and the data model via the context API (`hotdata context`), or interact with the hotdata service. Activate when the user says "run hotdata", "query hotdata", "list workspaces", "list connections", "create a connection", "list tables", "list datasets", "create a dataset", "upload a dataset", "execute a query", "search a table", "list indexes", "create an index", "list query runs", "list past queries", "query history", "list sandboxes", "create a sandbox", "run a sandbox", "workspace context", "pull context", "push context", "data model", or asks you to use the hotdata CLI.
Use when building, deploying, or debugging applications on Modal (modal.com) — serverless cloud compute for Python. Covers container images, GPU functions, web endpoints, volumes, secrets, scheduling, distributed training, calling deployed functions, and scaling patterns.
| name | modal-remote-functions |
| description | Use when calling deployed Modal functions from a CLI or external service without needing the Modal app running locally |
Modal functions deployed to the cloud can be invoked from external clients (CLI, web apps, services) using HTTP endpoints instead of .remote() calls. This eliminates the need for the Modal app context on the client side—the CLI becomes a simple HTTP consumer.
Core principle: Co-locate @modal.fastapi_endpoint() wrappers with workflow functions, pass Pydantic BaseModels directly to endpoints (FastAPI handles validation), and call endpoints via HTTP requests.
File: src/attio/people.py
import modal
from src.app import app, image
from libs.attio.models import PersonSearchInput # Pydantic BaseModel
# Modal workflow function (existing)
@app.function(image=image)
def attio_search_people(name: str = None, email: str = None, attio_api_key: str = "") -> list[PersonSearchResult]:
os.environ["ATTIO_API_KEY"] = attio_api_key
try:
return search_people(name=name, email=email)
finally:
os.environ.pop("ATTIO_API_KEY", None)
# HTTP endpoint takes BaseModel, FastAPI validates JSON automatically
@app.function(image=image)
@modal.fastapi_endpoint(method="POST", docs=True)
def http_attio_search_people(query: PersonSearchInput) -> list[dict]:
"""HTTP endpoint for attio_search_people."""
results = attio_search_people.remote(
name=query.name,
email=query.email,
attio_api_key=query.attio_api_key,
)
return [r.model_dump() for r in results]
Why this pattern:
PersonSearchInputFile: cli/attio/people.py
import httpx
from libs.attio.models import PersonSearchInput # Import BaseModel from libs
MODAL_ENDPOINT = "https://username--app-name.modal.run"
@app.command()
def search(name: str = None, email: str = None, attio_api_key: str = ""):
"""Search for people in Attio."""
query = PersonSearchInput(
name=name,
email=email,
attio_api_key=attio_api_key,
)
response = httpx.post(
f"{MODAL_ENDPOINT}/http_attio_search_people",
json=query.model_dump(),
)
response.raise_for_status()
print(json.dumps(response.json(), indent=2))
Benefits:
| Layer | Pattern | Purpose |
|---|---|---|
| Workflow | @app.function(image=image) | Actual Modal function, runs in cloud |
| Endpoint | @app.function() + @modal.fastapi_endpoint(method="POST") | HTTP wrapper, takes BaseModel, calls .remote() on workflow |
| CLI | PersonInput(...) + httpx.post(json=model.model_dump()) | HTTP client, imports and instantiates BaseModel |
Problem:
@modal.fastapi_endpoint(method="POST")
def http_endpoint(name: str = None, email: str = None, api_key: str = ""):
# Manual field extraction, duplicates model structure
pass
Why: Duplicates the BaseModel definition in endpoint signature, harder to maintain.
Fix: Accept BaseModel directly:
@modal.fastapi_endpoint(method="POST")
def http_endpoint(query: PersonSearchInput):
# FastAPI validates and deserializes automatically
pass
Problem:
response = httpx.get(
f"{MODAL_ENDPOINT}/http_endpoint",
params={"name": name, "email": email, "api_key": api_key}
)
Why: Query params don't validate against BaseModel; no FastAPI validation.
Fix: Use POST with JSON body:
query = PersonSearchInput(name=name, email=email, api_key=api_key)
response = httpx.post(
f"{MODAL_ENDPOINT}/http_endpoint",
json=query.model_dump(),
)
Problem:
@modal.fastapi_endpoint(method="POST")
def http_endpoint(query: PersonSearchInput):
result = attio_search_people(...) # Wrong: no .remote()
return result.model_dump()
Why: Without .remote(), function runs in endpoint's context instead of Modal cloud.
Fix: Always use .remote():
result = attio_search_people.remote(
name=query.name,
email=query.email,
attio_api_key=query.attio_api_key,
)
.remote() flagged by Pyright in endpoint wrappersProblem:
result = attio_search_people.remote(...)
Static analysis may report pyright/reportFunctionMemberAccess even though Modal adds .remote at runtime.
Fix (preferred in this repo):
result = attio_search_people.remote(...) # pyright: ignore[reportFunctionMemberAccess]
Trunk note: For Pyright issues, this inline Pyright suppression is more reliable than trunk-ignore(pyright/...) comments, which can show trunk/ignore-does-nothing in some configurations.
Problem:
@modal.fastapi_endpoint(method="POST") # Missing @app.function()
def http_endpoint(query: PersonSearchInput):
pass
Why: Endpoint won't be registered with Modal app or deployed.
Fix: Stack decorators in order:
@app.function(image=image)
@modal.fastapi_endpoint(method="POST", docs=True)
def http_endpoint(query: PersonSearchInput):
pass
@app.function() then @modal.fastapi_endpoint().remote().model_dump())httpx.post(..., json=model.model_dump())https://username--app-name.modal.run)modal deploy src/app.py (may need to touch file to force detection)curl -X POST https://username--app-name.modal.run/http_endpoint_name -H "Content-Type: application/json" -d '{...}'Function.from_name().remote()When the CLI already has the Modal SDK installed and authenticated, you can skip HTTP endpoints entirely and call deployed functions directly:
import modal
MODAL_APP = "my-app-name"
fn = modal.Function.from_name(MODAL_APP, "attio_search_people")
results = fn.remote(name="John", email=None, attio_api_key="sk-...")
Key rule: Pass parameters directly — do NOT wrap them in a BaseModel/Pydantic object. The .remote() call mirrors the function signature exactly.
# ✅ Correct — direct params
fn.remote(name="John", email=None, attio_api_key="sk-...")
# ❌ Wrong — BaseModel wrapping
query = PersonSearchQuery(name="John", email=None, attio_api_key="sk-...")
fn.remote(query) # Fails or produces unexpected results
Modal may return dicts OR Pydantic objects depending on whether the model class is importable in the calling process. The calling process may not have the same imports as the Modal container.
Always guard deserialization:
results = fn.remote(name="John")
for r in results:
if hasattr(r, "model_dump"):
data = r.model_dump() # Pydantic object
else:
data = r # Already a dict
If CLI files import from src.workflows_* modules (e.g., from src.parallel.findall import ...), those imports execute the @app.function decorators again, registering duplicate functions. This causes deployment errors or silent failures.
Fix: Query/input models should live in libs/ (shared pure code), NOT in the workflow files. CLI files should only import from libs/ for models, and use modal.Function.from_name() to reference the deployed functions — never import directly from src/ workflow modules.
# ✅ Correct — CLI imports model from libs, references function by name
from libs.attio.models import PersonSearchInput
fn = modal.Function.from_name(MODAL_APP, "attio_search_people")
# ❌ Wrong — importing from workflow registers duplicate @app.function
from src.parallel.findall import parallel_findall_create
python -mThe CLI must be run via python -m cli.main, not by executing submodules directly. Submodules don't have a top-level app() call (Typer entrypoint), so running them directly does nothing or errors.
# ✅ Correct
python -m cli.main attio people search --name "John"
# ❌ Wrong — no app() call at module level
python cli/attio/people.py