Show results your team can read: tables, reports and downloads
The person running your function never sees a terminal, so the result is the only thing they get. Callables picks how to show it from your function's return type. Get the annotation right and the result is a clean table or a formatted report instead of a block of JSON.
Tables: return a list of rows
Return list[dict] and the result is a table, with a button to download it as CSV:
@deploy(title="Overdue invoices")
def overdue(days: int = 30) -> list[dict]:
"""Invoices unpaid for more than the given number of days."""
return [
{"Customer": "Acme", "Amount": 1200, "Days late": 41},
{"Customer": "Globex", "Amount": 640, "Days late": 33},
]
The keys become column headers, so use readable names like "Days late" rather than days_late. Keep values flat: numbers, strings and dates, not nested objects. A list of Pydantic models or a pandas DataFrame works too.
Reports: return a string
Return str and it's shown as Markdown, so headings, lists, bold text, links and tables all work:
@deploy(title="Weekly summary")
def weekly_summary() -> str:
return "\n".join([
"## Week of Oct 12",
"- **42** new signups, up 12%",
"- **3** churned accounts",
"",
"[Open the dashboard](https://example.com/dashboard)",
])
This is the best fit for summaries people read, like a daily digest or the outcome of an action ("Refunded order 1234 and emailed the customer").
A single record: return a dict
Return dict or a Pydantic model and it's shown as a list of labels and values, which is right for looking up one thing:
@deploy(title="Look up a customer")
def customer(email: str) -> dict:
return {"Name": "Ann Lee", "Plan": "Pro", "Since": "2025-03-14"}
Files: return a File
Return File or list[File] and the person gets download buttons. Use save_file to add downloads next to any other result. See file uploads.
Nothing to show: return None
Annotate -> None and a successful run shows "Done". Good for actions where the run history is the record.
Always annotate the return type
Without an annotation, or with a type Callables doesn't recognize, the result is shown as raw JSON. It works, but it's the one display that isn't written for people. call check warns about it.
Go further: actions on each row
A table can offer buttons on each row that run another function with the row's values filled in, like "Refund" next to each order. See building an admin panel.
Turn your scripts into tools your team can run
Add @deploy to a Python function and push. Your team gets a form, schedules and run history.